diff options
author | IronClawTrem <louie.nutman@gmail.com> | 2020-02-16 03:40:06 +0000 |
---|---|---|
committer | IronClawTrem <louie.nutman@gmail.com> | 2020-02-16 03:40:06 +0000 |
commit | 425decdf7e9284d15aa726e3ae96b9942fb0e3ea (patch) | |
tree | 6c0dd7edfefff1be7b9e75fe0b3a0a85fe1595f3 /src/client | |
parent | ccb0b2e4d6674a7a00c9bf491f08fc73b6898c54 (diff) |
create tremded branch
Diffstat (limited to 'src/client')
39 files changed, 27077 insertions, 4 deletions
diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt new file mode 100644 index 0000000..c10fd9c --- /dev/null +++ b/src/client/CMakeLists.txt @@ -0,0 +1,209 @@ +# +## .o88b. db d888888b d88888b d8b db d888888b +## d8P Y8 88 `88' 88' 888o 88 `~~88~~' +## 8P 88 88 88ooooo 88V8o 88 88 +## 8b 88 88 88~~~~~ 88 V8o88 88 +## Y8b d8 88booo. .88. 88. 88 V888 88 +## `Y88P' Y88888P Y888888P Y88888P VP V8P YP +# + +find_package(CURL) +find_package(OpenGL) +find_package(OpenAL) +include(${CMAKE_SOURCE_DIR}/cmake/SDL2.cmake) + +add_definitions( + -DUSE_LOCAL_HEADERS + -DUSE_RESTCLIENT + -DUSE_CODEC_OPUS + -DUSE_OPENAL + -DUSE_OPENAL_DLOPEN + -DUSE_INTERNAL_JPEG + -DUSE_RENDERER_DLOPEN + -DNDEBUG + -DPRODUCT_VERSION="1.3.0 alpha" + ${SDL2_DEFINES} + ) + +set(PARENT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) +set(EXTERNAL_DIR ${CMAKE_SOURCE_DIR}/external) +if(APPLE) +set(APPLE_SOURCES ${PARENT_DIR}/sys/sys_osx.mm) +endif(APPLE) + +add_executable( + tremulous + # + ${PARENT_DIR}/asm/snapvector.c + # + cl_avi.cpp + cl_cgame.cpp + cl_cin.cpp + cl_console.cpp + cl_curl.cpp + cl_input.cpp + cl_keys.cpp + cl_main.cpp + cl_net_chan.cpp + cl_parse.cpp + cl_rest.cpp + cl_scrn.cpp + cl_ui.cpp + cl_updates.cpp + libmumblelink.cpp + qal.cpp + snd_adpcm.cpp + snd_codec.cpp + snd_codec_ogg.cpp + snd_codec_opus.cpp + snd_codec_wav.cpp + snd_dma.cpp + snd_main.cpp + snd_mem.cpp + snd_mix.cpp + snd_openal.cpp + snd_wavelet.cpp + # + ${PARENT_DIR}/qcommon/cm_load.cpp + ${PARENT_DIR}/qcommon/cm_patch.cpp + ${PARENT_DIR}/qcommon/cm_polylib.cpp + ${PARENT_DIR}/qcommon/cm_test.cpp + ${PARENT_DIR}/qcommon/cm_trace.cpp + ${PARENT_DIR}/qcommon/cmd.cpp + ${PARENT_DIR}/qcommon/common.cpp + ${PARENT_DIR}/qcommon/crypto.cpp + ${PARENT_DIR}/qcommon/cvar.cpp + ${PARENT_DIR}/qcommon/cvar.h + ${PARENT_DIR}/qcommon/files.cpp + ${PARENT_DIR}/qcommon/files.h + ${PARENT_DIR}/qcommon/huffman.cpp + ${PARENT_DIR}/qcommon/huffman.h + ${PARENT_DIR}/qcommon/ioapi.cpp + ${PARENT_DIR}/qcommon/md4.cpp + ${PARENT_DIR}/qcommon/md5.cpp + ${PARENT_DIR}/qcommon/msg.cpp + ${PARENT_DIR}/qcommon/msg.h + ${PARENT_DIR}/qcommon/net_chan.cpp + ${PARENT_DIR}/qcommon/net_ip.cpp + ${PARENT_DIR}/qcommon/net.h + ${PARENT_DIR}/qcommon/parse.cpp + ${PARENT_DIR}/qcommon/puff.cpp + ${PARENT_DIR}/qcommon/q_shared.c + ${PARENT_DIR}/qcommon/q3_lauxlib.cpp + ${PARENT_DIR}/qcommon/q_math.c + ${PARENT_DIR}/qcommon/unzip.cpp + ${PARENT_DIR}/qcommon/vm.cpp + ${PARENT_DIR}/qcommon/vm_interpreted.cpp + ${PARENT_DIR}/qcommon/vm_x86.cpp + # + ${PARENT_DIR}/sdl/sdl_input.cpp + ${PARENT_DIR}/sdl/sdl_snd.cpp + # + ${PARENT_DIR}/server/sv_ccmds.cpp + ${PARENT_DIR}/server/sv_client.cpp + ${PARENT_DIR}/server/sv_game.cpp + ${PARENT_DIR}/server/sv_init.cpp + ${PARENT_DIR}/server/sv_main.cpp + ${PARENT_DIR}/server/sv_net_chan.cpp + ${PARENT_DIR}/server/sv_snapshot.cpp + ${PARENT_DIR}/server/sv_world.cpp + # + ${PARENT_DIR}/sys/con_log.cpp + ${PARENT_DIR}/sys/con_tty.cpp + ${PARENT_DIR}/sys/sys_main.cpp + ${PARENT_DIR}/sys/sys_unix.cpp + ${PARENT_DIR}/sys/sys_shared.h + ${APPLE_SOURCES} + + ${EXTERNAL_DIR}/semver/src/lib/semantic_version_v1.cpp + ${EXTERNAL_DIR}/semver/src/lib/semantic_version_v2.cpp + ) + +if(APPLE) + # FIXME Prefixed with "lua" to prevent cmake from doing "-l-framework Cocoa" + set(FRAMEWORKS "-framework Cocoa -framework Security -framework OpenAL -framework IOKit") +else(APPLE) + if(UNIX) + set(SYSLIBS dl rt) + endif(UNIX) +endif(APPLE) + +if(NOT USE_RENDERER_DLOPEN) + if(USE_OPENGL1) + set(RENDERER_LIBRARY renderergl1) + endif(USE_OPENGL1) + set(RENDERER_LIBRARY renderergl2) +endif(NOT USE_RENDERER_DLOPEN) + +target_link_libraries( + tremulous + # + lua + nettle + zlib + ogg + opus + opusfile + # + script_api + restclient + # + ${FRAMEWORKS} + ${CURL_LIBRARIES} + ${SDL2_LIBRARIES} + ${OPENGL_LIBRARIES} + ${OPENAL_LIBRARY} + ${SYSLIBS} + ) + +include_directories( + ${PARENT_DIR} + ${PARENT_DIR}/qcommon + ${EXTERNAL_DIR}/zlib + ${EXTERNAL_DIR}/restclient + ${EXTERNAL_DIR}/rapidjson + ${EXTERNAL_DIR}/SDL2/include + ${EXTERNAL_DIR}/jpeg-8c + ${EXTERNAL_DIR}/libogg-1.3.2/include + ${EXTERNAL_DIR}/lua-5.3.3/include + ${EXTERNAL_DIR}/sol + ${EXTERNAL_DIR}/nettle-3.3 + ${EXTERNAL_DIR}/opus-1.1.4/include + ${EXTERNAL_DIR}/opusfile-0.8/include + ${EXTERNAL_DIR}/restclient + ${EXTERNAL_DIR}/semver/src/include + ${SDL2_INCLUDE_DIRS} + ${OPENAL_INCLUDE_DIR} + ) + +# TODO: Turn this into a macro +if (USE_INTERNAL_SDL2) +add_custom_command( + TARGET tremulous + POST_BUILD COMMAND ${CMAKE_COMMAND} + ARGS -E copy ${CMAKE_SOURCE_DIR}/external/SDL2/libs/Darwin/libSDL2-2.0.0.dylib ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/libSDL2-2.0.0.dylib + ) +endif(USE_INTERNAL_SDL2) + +if (USE_RENDERER_DLOPEN) + add_custom_command( + TARGET tremulous + POST_BUILD COMMAND ${CMAKE_COMMAND} + ARGS -E copy ${CMAKE_BINARY_DIR}/src/renderergl1/librenderergl1${CMAKE_SHARED_LIBRARY_SUFFIX} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/renderer_opengl1${CMAKE_SHARED_LIBRARY_SUFFIX} + ) + + add_custom_command( + TARGET tremulous + POST_BUILD COMMAND ${CMAKE_COMMAND} + ARGS -E copy ${CMAKE_BINARY_DIR}/src/renderergl2/librenderergl2${CMAKE_SHARED_LIBRARY_SUFFIX} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/renderer_opengl2${CMAKE_SHARED_LIBRARY_SUFFIX} + ) +endif(USE_RENDERER_DLOPEN) + +add_dependencies( + tremulous + ui + game + cgame + renderergl1 + renderergl2 + ) diff --git a/src/client/cl_avi.cpp b/src/client/cl_avi.cpp new file mode 100644 index 0000000..d533e60 --- /dev/null +++ b/src/client/cl_avi.cpp @@ -0,0 +1,664 @@ +/* +=========================================================================== +Copyright (C) 2005-2009 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#include "client.h" +#include "snd_local.h" + +#define INDEX_FILE_EXTENSION ".index.dat" + +#define MAX_RIFF_CHUNKS 16 + +typedef struct audioFormat_s +{ + int rate; + int format; + int channels; + int bits; + + int sampleSize; + int totalBytes; +} audioFormat_t; + +typedef struct aviFileData_s +{ + bool fileOpen; + fileHandle_t f; + char fileName[ MAX_QPATH ]; + int fileSize; + int moviOffset; + int moviSize; + + fileHandle_t idxF; + int numIndices; + + int frameRate; + int framePeriod; + int width, height; + int numVideoFrames; + int maxRecordSize; + bool motionJpeg; + + bool audio; + audioFormat_t a; + int numAudioFrames; + + int chunkStack[ MAX_RIFF_CHUNKS ]; + int chunkStackTop; + + byte *cBuffer, *eBuffer; +} aviFileData_t; + +static aviFileData_t afd; + +#define MAX_AVI_BUFFER 2048 + +static byte buffer[ MAX_AVI_BUFFER ]; +static int bufIndex; + +/* +=============== +SafeFS_Write +=============== +*/ +static ID_INLINE void SafeFS_Write( const void *buffer, int len, fileHandle_t f ) +{ + if( FS_Write( buffer, len, f ) < len ) + Com_Error( ERR_DROP, "Failed to write avi file" ); +} + +/* +=============== +WRITE_STRING +=============== +*/ +static ID_INLINE void WRITE_STRING( const char *s ) +{ + ::memcpy( &buffer[ bufIndex ], s, strlen( s ) ); + bufIndex += strlen( s ); +} + +/* +=============== +WRITE_4BYTES +=============== +*/ +static ID_INLINE void WRITE_4BYTES( int x ) +{ + buffer[ bufIndex + 0 ] = (byte)( ( x >> 0 ) & 0xFF ); + buffer[ bufIndex + 1 ] = (byte)( ( x >> 8 ) & 0xFF ); + buffer[ bufIndex + 2 ] = (byte)( ( x >> 16 ) & 0xFF ); + buffer[ bufIndex + 3 ] = (byte)( ( x >> 24 ) & 0xFF ); + bufIndex += 4; +} + +/* +=============== +WRITE_2BYTES +=============== +*/ +static ID_INLINE void WRITE_2BYTES( int x ) +{ + buffer[ bufIndex + 0 ] = (byte)( ( x >> 0 ) & 0xFF ); + buffer[ bufIndex + 1 ] = (byte)( ( x >> 8 ) & 0xFF ); + bufIndex += 2; +} + +/* +=============== +START_CHUNK +=============== +*/ +static ID_INLINE void START_CHUNK( const char *s ) +{ + if( afd.chunkStackTop == MAX_RIFF_CHUNKS ) + { + Com_Error( ERR_DROP, "ERROR: Top of chunkstack breached" ); + } + + afd.chunkStack[ afd.chunkStackTop ] = bufIndex; + afd.chunkStackTop++; + WRITE_STRING( s ); + WRITE_4BYTES( 0 ); +} + +/* +=============== +END_CHUNK +=============== +*/ +static ID_INLINE void END_CHUNK( void ) +{ + int endIndex = bufIndex; + + if( afd.chunkStackTop <= 0 ) + { + Com_Error( ERR_DROP, "ERROR: Bottom of chunkstack breached" ); + } + + afd.chunkStackTop--; + bufIndex = afd.chunkStack[ afd.chunkStackTop ]; + bufIndex += 4; + WRITE_4BYTES( endIndex - bufIndex - 4 ); + bufIndex = endIndex; + bufIndex = PAD( bufIndex, 2 ); +} + +/* +=============== +CL_WriteAVIHeader +=============== +*/ +void CL_WriteAVIHeader( void ) +{ + bufIndex = 0; + afd.chunkStackTop = 0; + + START_CHUNK( "RIFF" ); + { + WRITE_STRING( "AVI " ); + { + START_CHUNK( "LIST" ); + { + WRITE_STRING( "hdrl" ); + WRITE_STRING( "avih" ); + WRITE_4BYTES( 56 ); //"avih" "chunk" size + WRITE_4BYTES( afd.framePeriod ); //dwMicroSecPerFrame + WRITE_4BYTES( afd.maxRecordSize * + afd.frameRate ); //dwMaxBytesPerSec + WRITE_4BYTES( 0 ); //dwReserved1 + WRITE_4BYTES( 0x110 ); //dwFlags bits HAS_INDEX and IS_INTERLEAVED + WRITE_4BYTES( afd.numVideoFrames ); //dwTotalFrames + WRITE_4BYTES( 0 ); //dwInitialFrame + + if( afd.audio ) //dwStreams + WRITE_4BYTES( 2 ); + else + WRITE_4BYTES( 1 ); + + WRITE_4BYTES( afd.maxRecordSize ); //dwSuggestedBufferSize + WRITE_4BYTES( afd.width ); //dwWidth + WRITE_4BYTES( afd.height ); //dwHeight + WRITE_4BYTES( 0 ); //dwReserved[ 0 ] + WRITE_4BYTES( 0 ); //dwReserved[ 1 ] + WRITE_4BYTES( 0 ); //dwReserved[ 2 ] + WRITE_4BYTES( 0 ); //dwReserved[ 3 ] + + START_CHUNK( "LIST" ); + { + WRITE_STRING( "strl" ); + WRITE_STRING( "strh" ); + WRITE_4BYTES( 56 ); //"strh" "chunk" size + WRITE_STRING( "vids" ); + + if( afd.motionJpeg ) + WRITE_STRING( "MJPG" ); + else + WRITE_4BYTES( 0 ); // BI_RGB + + WRITE_4BYTES( 0 ); //dwFlags + WRITE_4BYTES( 0 ); //dwPriority + WRITE_4BYTES( 0 ); //dwInitialFrame + + WRITE_4BYTES( 1 ); //dwTimescale + WRITE_4BYTES( afd.frameRate ); //dwDataRate + WRITE_4BYTES( 0 ); //dwStartTime + WRITE_4BYTES( afd.numVideoFrames ); //dwDataLength + + WRITE_4BYTES( afd.maxRecordSize ); //dwSuggestedBufferSize + WRITE_4BYTES( -1 ); //dwQuality + WRITE_4BYTES( 0 ); //dwSampleSize + WRITE_2BYTES( 0 ); //rcFrame + WRITE_2BYTES( 0 ); //rcFrame + WRITE_2BYTES( afd.width ); //rcFrame + WRITE_2BYTES( afd.height ); //rcFrame + + WRITE_STRING( "strf" ); + WRITE_4BYTES( 40 ); //"strf" "chunk" size + WRITE_4BYTES( 40 ); //biSize + WRITE_4BYTES( afd.width ); //biWidth + WRITE_4BYTES( afd.height ); //biHeight + WRITE_2BYTES( 1 ); //biPlanes + WRITE_2BYTES( 24 ); //biBitCount + + if( afd.motionJpeg ) //biCompression + { + WRITE_STRING( "MJPG" ); + WRITE_4BYTES( afd.width * + afd.height ); //biSizeImage + } + else + { + WRITE_4BYTES( 0 ); // BI_RGB + WRITE_4BYTES( afd.width * + afd.height * 3 ); //biSizeImage + } + + WRITE_4BYTES( 0 ); //biXPelsPetMeter + WRITE_4BYTES( 0 ); //biYPelsPetMeter + WRITE_4BYTES( 0 ); //biClrUsed + WRITE_4BYTES( 0 ); //biClrImportant + } + END_CHUNK( ); + + if( afd.audio ) + { + START_CHUNK( "LIST" ); + { + WRITE_STRING( "strl" ); + WRITE_STRING( "strh" ); + WRITE_4BYTES( 56 ); //"strh" "chunk" size + WRITE_STRING( "auds" ); + WRITE_4BYTES( 0 ); //FCC + WRITE_4BYTES( 0 ); //dwFlags + WRITE_4BYTES( 0 ); //dwPriority + WRITE_4BYTES( 0 ); //dwInitialFrame + + WRITE_4BYTES( afd.a.sampleSize ); //dwTimescale + WRITE_4BYTES( afd.a.sampleSize * + afd.a.rate ); //dwDataRate + WRITE_4BYTES( 0 ); //dwStartTime + WRITE_4BYTES( afd.a.totalBytes / + afd.a.sampleSize ); //dwDataLength + + WRITE_4BYTES( 0 ); //dwSuggestedBufferSize + WRITE_4BYTES( -1 ); //dwQuality + WRITE_4BYTES( afd.a.sampleSize ); //dwSampleSize + WRITE_2BYTES( 0 ); //rcFrame + WRITE_2BYTES( 0 ); //rcFrame + WRITE_2BYTES( 0 ); //rcFrame + WRITE_2BYTES( 0 ); //rcFrame + + WRITE_STRING( "strf" ); + WRITE_4BYTES( 18 ); //"strf" "chunk" size + WRITE_2BYTES( afd.a.format ); //wFormatTag + WRITE_2BYTES( afd.a.channels ); //nChannels + WRITE_4BYTES( afd.a.rate ); //nSamplesPerSec + WRITE_4BYTES( afd.a.sampleSize * + afd.a.rate ); //nAvgBytesPerSec + WRITE_2BYTES( afd.a.sampleSize ); //nBlockAlign + WRITE_2BYTES( afd.a.bits ); //wBitsPerSample + WRITE_2BYTES( 0 ); //cbSize + } + END_CHUNK( ); + } + } + END_CHUNK( ); + + afd.moviOffset = bufIndex; + + START_CHUNK( "LIST" ); + { + WRITE_STRING( "movi" ); + } + } + } +} + +/* +=============== +CL_OpenAVIForWriting + +Creates an AVI file and gets it into a state where +writing the actual data can begin +=============== +*/ +bool CL_OpenAVIForWriting( const char *fileName ) +{ + if( afd.fileOpen ) + return false; + + ::memset( &afd, 0, sizeof( aviFileData_t ) ); + + // Don't start if a framerate has not been chosen + if( cl_aviFrameRate->integer <= 0 ) + { + Com_Printf( S_COLOR_RED "cl_aviFrameRate must be >= 1\n" ); + return false; + } + + if( ( afd.f = FS_FOpenFileWrite( fileName ) ) <= 0 ) + return false; + + if( ( afd.idxF = FS_FOpenFileWrite( + va( "%s" INDEX_FILE_EXTENSION, fileName ) ) ) <= 0 ) + { + FS_FCloseFile( afd.f ); + return false; + } + + Q_strncpyz( afd.fileName, fileName, MAX_QPATH ); + + afd.frameRate = cl_aviFrameRate->integer; + afd.framePeriod = (int)( 1000000.0f / afd.frameRate ); + afd.width = cls.glconfig.vidWidth; + afd.height = cls.glconfig.vidHeight; + + if( cl_aviMotionJpeg->integer ) + afd.motionJpeg = true; + else + afd.motionJpeg = false; + + // Buffers only need to store RGB pixels. + // Allocate a bit more space for the capture buffer to account for possible + // padding at the end of pixel lines, and padding for alignment + #define MAX_PACK_LEN 16 + afd.cBuffer = (byte*)Z_Malloc((afd.width * 3 + MAX_PACK_LEN - 1) * afd.height + MAX_PACK_LEN - 1); + // raw avi files have pixel lines start on 4-byte boundaries + afd.eBuffer = (byte*)Z_Malloc(PAD(afd.width * 3, AVI_LINE_PADDING) * afd.height); + + afd.a.rate = dma.speed; + afd.a.format = WAV_FORMAT_PCM; + afd.a.channels = dma.channels; + afd.a.bits = dma.samplebits; + afd.a.sampleSize = ( afd.a.bits / 8 ) * afd.a.channels; + + if( afd.a.rate % afd.frameRate ) + { + int suggestRate = afd.frameRate; + + while( ( afd.a.rate % suggestRate ) && suggestRate >= 1 ) + suggestRate--; + + Com_Printf( S_COLOR_YELLOW "WARNING: cl_aviFrameRate is not a divisor " + "of the audio rate, suggest %d\n", suggestRate ); + } + + if( !Cvar_VariableIntegerValue( "s_initsound" ) ) + { + afd.audio = false; + } + else if( Q_stricmp( Cvar_VariableString( "s_backend" ), "OpenAL" ) ) + { + if( afd.a.bits != 16 || afd.a.channels != 2 ) + { + Com_Printf( S_COLOR_YELLOW "WARNING: Audio format of %d bit/%d channels not supported", + afd.a.bits, afd.a.channels ); + afd.audio = false; + } + else + afd.audio = true; + } + else + { + afd.audio = false; + Com_Printf( S_COLOR_YELLOW "WARNING: Audio capture is not supported " + "with OpenAL. Set s_useOpenAL to 0 for audio capture\n" ); + } + + // This doesn't write a real header, but allocates the + // correct amount of space at the beginning of the file + CL_WriteAVIHeader( ); + + SafeFS_Write( buffer, bufIndex, afd.f ); + afd.fileSize = bufIndex; + + bufIndex = 0; + START_CHUNK( "idx1" ); + SafeFS_Write( buffer, bufIndex, afd.idxF ); + + afd.moviSize = 4; // For the "movi" + afd.fileOpen = true; + + return true; +} + +/* +=============== +CL_CheckFileSize +=============== +*/ +static bool CL_CheckFileSize( int bytesToAdd ) +{ + unsigned int newFileSize; + + newFileSize = + afd.fileSize + // Current file size + bytesToAdd + // What we want to add + ( afd.numIndices * 16 ) + // The index + 4; // The index size + + // I assume all the operating systems + // we target can handle a 2Gb file + if( newFileSize > INT_MAX ) + { + // Close the current file... + CL_CloseAVI( ); + + // ...And open a new one + CL_OpenAVIForWriting( va( "%s_", afd.fileName ) ); + + return true; + } + + return false; +} + +/* +=============== +CL_WriteAVIVideoFrame +=============== +*/ +void CL_WriteAVIVideoFrame( const byte *imageBuffer, int size ) +{ + int chunkOffset = afd.fileSize - afd.moviOffset - 8; + int chunkSize = 8 + size; + int paddingSize = PADLEN(size, 2); + byte padding[ 4 ] = { 0 }; + + if( !afd.fileOpen ) + return; + + // Chunk header + contents + padding + if( CL_CheckFileSize( 8 + size + 2 ) ) + return; + + bufIndex = 0; + WRITE_STRING( "00dc" ); + WRITE_4BYTES( size ); + + SafeFS_Write( buffer, 8, afd.f ); + SafeFS_Write( imageBuffer, size, afd.f ); + SafeFS_Write( padding, paddingSize, afd.f ); + afd.fileSize += ( chunkSize + paddingSize ); + + afd.numVideoFrames++; + afd.moviSize += ( chunkSize + paddingSize ); + + if( size > afd.maxRecordSize ) + afd.maxRecordSize = size; + + // Index + bufIndex = 0; + WRITE_STRING( "00dc" ); //dwIdentifier + WRITE_4BYTES( 0x00000010 ); //dwFlags (all frames are KeyFrames) + WRITE_4BYTES( chunkOffset ); //dwOffset + WRITE_4BYTES( size ); //dwLength + SafeFS_Write( buffer, 16, afd.idxF ); + + afd.numIndices++; +} + +#define PCM_BUFFER_SIZE 44100 + +/* +=============== +CL_WriteAVIAudioFrame +=============== +*/ +void CL_WriteAVIAudioFrame( const byte *pcmBuffer, int size ) +{ + static byte pcmCaptureBuffer[ PCM_BUFFER_SIZE ] = { 0 }; + static int bytesInBuffer = 0; + + if( !afd.audio ) + return; + + if( !afd.fileOpen ) + return; + + // Chunk header + contents + padding + if( CL_CheckFileSize( 8 + bytesInBuffer + size + 2 ) ) + return; + + if( bytesInBuffer + size > PCM_BUFFER_SIZE ) + { + Com_Printf( S_COLOR_YELLOW + "WARNING: Audio capture buffer overflow -- truncating\n" ); + size = PCM_BUFFER_SIZE - bytesInBuffer; + } + + ::memcpy( &pcmCaptureBuffer[ bytesInBuffer ], pcmBuffer, size ); + bytesInBuffer += size; + + // Only write if we have a frame's worth of audio + if( bytesInBuffer >= (int)ceil( (float)afd.a.rate / (float)afd.frameRate ) * + afd.a.sampleSize ) + { + int chunkOffset = afd.fileSize - afd.moviOffset - 8; + int chunkSize = 8 + bytesInBuffer; + int paddingSize = PADLEN(bytesInBuffer, 2); + byte padding[ 4 ] = { 0 }; + + bufIndex = 0; + WRITE_STRING( "01wb" ); + WRITE_4BYTES( bytesInBuffer ); + + SafeFS_Write( buffer, 8, afd.f ); + SafeFS_Write( pcmCaptureBuffer, bytesInBuffer, afd.f ); + SafeFS_Write( padding, paddingSize, afd.f ); + afd.fileSize += ( chunkSize + paddingSize ); + + afd.numAudioFrames++; + afd.moviSize += ( chunkSize + paddingSize ); + afd.a.totalBytes += bytesInBuffer; + + // Index + bufIndex = 0; + WRITE_STRING( "01wb" ); //dwIdentifier + WRITE_4BYTES( 0 ); //dwFlags + WRITE_4BYTES( chunkOffset ); //dwOffset + WRITE_4BYTES( bytesInBuffer ); //dwLength + SafeFS_Write( buffer, 16, afd.idxF ); + + afd.numIndices++; + + bytesInBuffer = 0; + } +} + +/* +=============== +CL_TakeVideoFrame +=============== +*/ +void CL_TakeVideoFrame( void ) +{ + // AVI file isn't open + if( !afd.fileOpen ) + return; + + re.TakeVideoFrame( afd.width, afd.height, afd.cBuffer, afd.eBuffer, afd.motionJpeg ); +} + +/* +=============== +CL_CloseAVI + +Closes the AVI file and writes an index chunk +=============== +*/ +bool CL_CloseAVI( void ) +{ + int indexRemainder; + int indexSize = afd.numIndices * 16; + const char *idxFileName = va( "%s" INDEX_FILE_EXTENSION, afd.fileName ); + + // AVI file isn't open + if( !afd.fileOpen ) + return false; + + afd.fileOpen = false; + + FS_Seek( afd.idxF, 4, FS_SEEK_SET ); + bufIndex = 0; + WRITE_4BYTES( indexSize ); + SafeFS_Write( buffer, bufIndex, afd.idxF ); + FS_FCloseFile( afd.idxF ); + + // Write index + + // Open the temp index file + if( ( indexSize = FS_FOpenFileRead( idxFileName, &afd.idxF, true ) ) <= 0 ) + { + FS_FCloseFile( afd.f ); + return false; + } + + indexRemainder = indexSize; + + // Append index to end of avi file + while( indexRemainder > MAX_AVI_BUFFER ) + { + FS_Read( buffer, MAX_AVI_BUFFER, afd.idxF ); + SafeFS_Write( buffer, MAX_AVI_BUFFER, afd.f ); + afd.fileSize += MAX_AVI_BUFFER; + indexRemainder -= MAX_AVI_BUFFER; + } + FS_Read( buffer, indexRemainder, afd.idxF ); + SafeFS_Write( buffer, indexRemainder, afd.f ); + afd.fileSize += indexRemainder; + FS_FCloseFile( afd.idxF ); + + // Remove temp index file + FS_HomeRemove( idxFileName ); + + // Write the real header + FS_Seek( afd.f, 0, FS_SEEK_SET ); + CL_WriteAVIHeader( ); + + bufIndex = 4; + WRITE_4BYTES( afd.fileSize - 8 ); // "RIFF" size + + bufIndex = afd.moviOffset + 4; // Skip "LIST" + WRITE_4BYTES( afd.moviSize ); + + SafeFS_Write( buffer, bufIndex, afd.f ); + + Z_Free( afd.cBuffer ); + Z_Free( afd.eBuffer ); + FS_FCloseFile( afd.f ); + + Com_Printf( "Wrote %d:%d frames to %s\n", afd.numVideoFrames, afd.numAudioFrames, afd.fileName ); + + return true; +} + +/* +=============== +CL_VideoRecording +=============== +*/ +bool CL_VideoRecording( void ) +{ + return afd.fileOpen; +} diff --git a/src/client/cl_cgame.cpp b/src/client/cl_cgame.cpp new file mode 100644 index 0000000..f16dc35 --- /dev/null +++ b/src/client/cl_cgame.cpp @@ -0,0 +1,1172 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +// cl_cgame.c -- client system interaction with client game + +#include "client.h" + +#ifdef USE_MUMBLE +#include "libmumblelink.h" +#endif +#include "snd_public.h" + +/* +==================== +CL_GetGameState +==================== +*/ +static void CL_GetGameState( gameState_t *gs ) +{ + *gs = cl.gameState; +} + +/* +==================== +CL_GetGlconfig +==================== +*/ +static void CL_GetGlconfig( glconfig_t *glconfig ) +{ + *glconfig = cls.glconfig; +} + + +/* +==================== +CL_GetUserCmd +==================== +*/ +static bool CL_GetUserCmd( int cmdNumber, usercmd_t *ucmd ) +{ + // cmds[cmdNumber] is the last properly generated command + + // can't return anything that we haven't created yet + if ( cmdNumber > cl.cmdNumber ) + Com_Error( ERR_DROP, "CL_GetUserCmd: %i >= %i", cmdNumber, cl.cmdNumber ); + + // the usercmd has been overwritten in the wrapping + // buffer because it is too far out of date + if ( cmdNumber <= cl.cmdNumber - CMD_BACKUP ) + return false; + + *ucmd = cl.cmds[ cmdNumber & CMD_MASK ]; + + return true; +} + +static int CL_GetCurrentCmdNumber( void ) +{ + return cl.cmdNumber; +} + +/* +==================== +CL_GetCurrentSnapshotNumber +==================== +*/ +static void CL_GetCurrentSnapshotNumber( int *snapshotNumber, int *serverTime ) +{ + *snapshotNumber = cl.snap.messageNum; + *serverTime = cl.snap.serverTime; +} + +/* +==================== +CL_GetSnapshot +==================== +*/ +bool CL_GetSnapshot( int snapshotNumber, snapshot_t *snapshot ) +{ + clSnapshot_t *clSnap; + + if ( snapshotNumber > cl.snap.messageNum ) { + Com_Error( ERR_DROP, "CL_GetSnapshot: snapshotNumber > cl.snapshot.messageNum" ); + } + + // if the frame has fallen out of the circular buffer, we can't return it + if ( cl.snap.messageNum - snapshotNumber >= PACKET_BACKUP ) + return false; + + // if the frame is not valid, we can't return it + clSnap = &cl.snapshots[snapshotNumber & PACKET_MASK]; + if ( !clSnap->valid ) + return false; + + // if the entities in the frame have fallen out of their circular buffer, + // we can't return it + if ( cl.parseEntitiesNum - clSnap->parseEntitiesNum >= MAX_PARSE_ENTITIES ) + return false; + + // write the snapshot + snapshot->snapFlags = clSnap->snapFlags; + snapshot->ping = clSnap->ping; + snapshot->serverTime = clSnap->serverTime; + ::memcpy( snapshot->areamask, clSnap->areamask, sizeof( snapshot->areamask ) ); + + int count = clSnap->numEntities; + if ( count > MAX_ENTITIES_IN_SNAPSHOT ) + { + Com_DPrintf( "CL_GetSnapshot: truncated %i entities to %i\n", count, MAX_ENTITIES_IN_SNAPSHOT ); + count = MAX_ENTITIES_IN_SNAPSHOT; + } + + if ( cls.cgInterface == 2 ) + { + alternateSnapshot_t *altSnapshot = (alternateSnapshot_t *)snapshot; + altSnapshot->ps = clSnap->alternatePs; + altSnapshot->serverCommandSequence = clSnap->serverCommandNum; + altSnapshot->numEntities = count; + + for ( int i = 0 ; i < count ; i++ ) + { + entityState_t *es = &cl.parseEntities[ ( clSnap->parseEntitiesNum + i ) & (MAX_PARSE_ENTITIES-1) ]; + ::memcpy( &altSnapshot->entities[i], es, (size_t)&((entityState_t *)0)->weaponAnim ); + altSnapshot->entities[i].generic1 = es->generic1; + } + } + else + { + snapshot->ps = clSnap->ps; + snapshot->serverCommandSequence = clSnap->serverCommandNum; + snapshot->numEntities = count; + for ( int i = 0 ; i < count ; i++ ) + { + snapshot->entities[i] = + cl.parseEntities[ ( clSnap->parseEntitiesNum + i ) & (MAX_PARSE_ENTITIES-1) ]; + } + } + + // FIXME: configstring changes and server commands!!! + + return true; +} + +/* +===================== +CL_SetUserCmdValue +===================== +*/ +void CL_SetUserCmdValue( int userCmdValue, float sensitivityScale ) { + cl.cgameUserCmdValue = userCmdValue; + cl.cgameSensitivity = sensitivityScale; +} + +/* +===================== +CL_AddCgameCommand +===================== +*/ +void CL_AddCgameCommand( const char *cmdName ) { + Cmd_AddCommand( cmdName, NULL ); +} + +/* +===================== +CL_ConfigstringModified +===================== +*/ +void CL_ConfigstringModified( void ) +{ + int idx = atoi( Cmd_Argv(1) ); + if ( idx < 0 || idx >= MAX_CONFIGSTRINGS ) + Com_Error( ERR_DROP, "CL_ConfigstringModified: bad index %i", idx ); + + // get everything after "cs <num>" + const char* s = Cmd_ArgsFrom(2); + const char* old = cl.gameState.stringData + cl.gameState.stringOffsets[ idx ]; + if ( !strcmp(old, s) ) + return; + + // build the new gameState_t + gameState_t oldGs = cl.gameState; + + ::memset( &cl.gameState, 0, sizeof( cl.gameState ) ); + + // leave the first 0 for uninitialized strings + cl.gameState.dataCount = 1; + + const char* dup; + for ( int i = 0 ; i < MAX_CONFIGSTRINGS ; i++ ) + { + if ( i == idx ) + dup = s; + else + dup = oldGs.stringData + oldGs.stringOffsets[ i ]; + + if ( !dup[0] ) + continue; // leave with the default empty string + + int len = strlen(dup); + if ( len + 1 + cl.gameState.dataCount > MAX_GAMESTATE_CHARS ) + Com_Error(ERR_DROP, "MAX_GAMESTATE_CHARS exceeded"); + + // append it to the gameState string buffer + cl.gameState.stringOffsets[ i ] = cl.gameState.dataCount; + ::memcpy( cl.gameState.stringData + cl.gameState.dataCount, dup, len + 1 ); + cl.gameState.dataCount += len + 1; + } + + if ( idx == CS_SYSTEMINFO ) + { + // parse serverId and other cvars + CL_SystemInfoChanged(); + } +} + + +/* +=================== +CL_GetServerCommand + +Set up argc/argv for the given command +=================== +*/ +static bool CL_GetServerCommand( int serverCommandNumber ) +{ + const char *s; + const char *cmd; + static char bigConfigString[BIG_INFO_STRING]; + int argc; + + // if we have irretrievably lost a reliable command, drop the connection + if ( serverCommandNumber <= clc.serverCommandSequence - MAX_RELIABLE_COMMANDS ) + { + // when a demo record was started after the client got a whole bunch of + // reliable commands then the client never got those first reliable commands + if ( clc.demoplaying ) + return false; + + Com_Error( ERR_DROP, "CL_GetServerCommand: a reliable command was cycled out" ); + return false; + } + + if ( serverCommandNumber > clc.serverCommandSequence ) + { + Com_Error( ERR_DROP, "CL_GetServerCommand: requested a command not received" ); + return false; + } + + s = clc.serverCommands[ serverCommandNumber & ( MAX_RELIABLE_COMMANDS - 1 ) ]; + clc.lastExecutedServerCommand = serverCommandNumber; + + Com_DPrintf( "serverCommand: %i : %s\n", serverCommandNumber, s ); + +rescan: + Cmd_TokenizeString( s ); + cmd = Cmd_Argv(0); + argc = Cmd_Argc(); + + if ( !strcmp( cmd, "disconnect" ) ) + { + // allow server to indicate why they were disconnected + if ( argc >= 2 ) + Com_Error( ERR_SERVERDISCONNECT, "Server disconnected - %s", Cmd_Argv( 1 ) ); + else + Com_Error( ERR_SERVERDISCONNECT, "Server disconnected" ); + } + + if ( !strcmp( cmd, "bcs0" ) ) + { + Com_sprintf( bigConfigString, BIG_INFO_STRING, "cs %s \"%s", Cmd_Argv(1), Cmd_Argv(2) ); + return false; + } + + if ( !strcmp( cmd, "bcs1" ) ) + { + s = Cmd_Argv(2); + if( strlen(bigConfigString) + strlen(s) >= BIG_INFO_STRING ) + Com_Error( ERR_DROP, "bcs exceeded BIG_INFO_STRING" ); + + strcat( bigConfigString, s ); + return false; + } + + if ( !strcmp( cmd, "bcs2" ) ) + { + s = Cmd_Argv(2); + if( strlen(bigConfigString) + strlen(s) + 1 >= BIG_INFO_STRING ) + Com_Error( ERR_DROP, "bcs exceeded BIG_INFO_STRING" ); + + strcat( bigConfigString, s ); + strcat( bigConfigString, "\"" ); + s = bigConfigString; + goto rescan; + } + + if ( !strcmp( cmd, "cs" ) ) + { + CL_ConfigstringModified(); + // reparse the string, because CL_ConfigstringModified may have done + // another Cmd_TokenizeString() + Cmd_TokenizeString( s ); + return true; + } + + if ( !strcmp( cmd, "map_restart" ) ) + { + // clear notify lines and outgoing commands before passing + // the restart to the cgame + Con_ClearNotify(); + // reparse the string, because Con_ClearNotify() may have done another + // Cmd_TokenizeString() + Cmd_TokenizeString( s ); + ::memset( cl.cmds, 0, sizeof( cl.cmds ) ); + return true; + } + + // we may want to put a "connect to other server" command here + + // cgame can now act on the command + return true; +} + + +/* +==================== +CL_CM_LoadMap + +Just adds default parameters that cgame doesn't need to know about +==================== +*/ +void CL_CM_LoadMap( const char *mapname ) { + int checksum; + + CM_LoadMap( mapname, true, &checksum ); +} + +char * safe_strncpy(char *dest, const char *src, size_t n) +{ + char *ret = dest; + while (n > 0 && src[0]) + { + *ret++ = *src++; + --n; + } + while (n > 0) + { + *ret++ = '\0'; + --n; + } + return dest; +} + +/* +==================== +CL_ShutdonwCGame + +==================== +*/ +void CL_ShutdownCGame( void ) { + Key_SetCatcher( Key_GetCatcher( ) & ~KEYCATCH_CGAME ); + cls.cgameStarted = false; + if ( !cls.cgame ) { + return; + } + VM_Call( cls.cgame, CG_SHUTDOWN ); + VM_Free( cls.cgame ); + cls.cgame = NULL; +} + +static int FloatAsInt( float f ) { + floatint_t fi; + fi.f = f; + return fi.i; +} + +static bool probingCG = false; + +/* +==================== +CL_CgameSystemCalls + +The cgame module is making a system call +==================== +*/ +intptr_t CL_CgameSystemCalls( intptr_t *args ) +{ + if( cls.cgInterface == 2 && args[0] >= CG_R_SETCLIPREGION && args[0] < CG_MEMSET ) + { + if( args[0] < CG_S_STOPBACKGROUNDTRACK - 1 ) + args[0] += 1; + + else if( args[0] < CG_S_STOPBACKGROUNDTRACK + 4 ) + args[0] += CG_PARSE_ADD_GLOBAL_DEFINE - CG_S_STOPBACKGROUNDTRACK + 1; + + else if( args[0] < CG_PARSE_ADD_GLOBAL_DEFINE + 4 ) + args[0] -= 4; + + else if( args[0] >= CG_PARSE_SOURCE_FILE_AND_LINE && args[0] <= CG_S_SOUNDDURATION ) + args[0] = CG_PARSE_SOURCE_FILE_AND_LINE - 1337 - args[0] ; + } + + switch( args[0] ) + { + case CG_PRINT: + Com_Printf( "%s", (const char*)VMA(1) ); + return 0; + case CG_ERROR: + if( probingCG ) + { + cls.cgInterface = 2; // this is a 1.1.0 cgame + return 0; + } + Com_Error( ERR_DROP, "%s", (const char*)VMA(1) ); + return 0; + case CG_MILLISECONDS: + return Sys_Milliseconds(); + case CG_CVAR_REGISTER: + Cvar_Register( (vmCvar_t*)VMA(1), (const char*)VMA(2), (const char*)VMA(3), args[4] ); + return 0; + case CG_CVAR_UPDATE: + Cvar_Update( (vmCvar_t*)VMA(1) ); + return 0; + case CG_CVAR_SET: + Cvar_SetSafe( (const char*)VMA(1), (const char*)VMA(2) ); + return 0; + case CG_CVAR_VARIABLESTRINGBUFFER: + Cvar_VariableStringBuffer( (const char*)VMA(1), (char*)VMA(2), args[3] ); + return 0; + case CG_ARGC: + return Cmd_Argc(); + case CG_ARGV: + Cmd_ArgvBuffer( args[1], (char*)VMA(2), args[3] ); + return 0; + case CG_ARGS: + Cmd_ArgsBuffer( (char*)VMA(1), args[2] ); + return 0; + case CG_LITERAL_ARGS: + Cmd_LiteralArgsBuffer( (char*)VMA(1), args[2] ); + return 0; + case CG_FS_FOPENFILE: + return FS_FOpenFileByMode( (const char*)VMA(1), (fileHandle_t*)VMA(2), (FS_Mode)args[3] ); + case CG_FS_READ: + FS_Read( VMA(1), args[2], args[3] ); + return 0; + case CG_FS_WRITE: + FS_Write( VMA(1), args[2], args[3] ); + return 0; + case CG_FS_FCLOSEFILE: + FS_FCloseFile( args[1] ); + return 0; + case CG_FS_SEEK: + return FS_Seek( (fileHandle_t)args[1], args[2], (FS_Origin)args[3] ); + case CG_FS_GETFILELIST: + return FS_GetFileList( (const char*)VMA(1), (const char*)VMA(2), (char*)VMA(3), args[4] ); + case CG_SENDCONSOLECOMMAND: + Cbuf_AddText( (const char*)VMA(1) ); + return 0; + case CG_ADDCOMMAND: + CL_AddCgameCommand( (const char*)VMA(1) ); + return 0; + case CG_REMOVECOMMAND: + Cmd_RemoveCommandSafe( (const char*)VMA(1) ); + return 0; + case CG_SENDCLIENTCOMMAND: + CL_AddReliableCommand((const char*)VMA(1), false); + return 0; + case CG_UPDATESCREEN: + // this is used during lengthy level loading, so pump message loop + // Com_EventLoop(); // FIXME: if a server restarts here, BAD THINGS HAPPEN! + // We can't call Com_EventLoop here, a restart will crash and this _does_ happen + // if there is a map change while we are downloading at pk3. + // ZOID + SCR_UpdateScreen(); + return 0; + case CG_CM_LOADMAP: + CL_CM_LoadMap( (const char*)VMA(1) ); + return 0; + case CG_CM_NUMINLINEMODELS: + return CM_NumInlineModels(); + case CG_CM_INLINEMODEL: + return CM_InlineModel( args[1] ); + case CG_CM_TEMPBOXMODEL: + return CM_TempBoxModel( (const float*)VMA(1), (const float*)VMA(2), false ); + case CG_CM_TEMPCAPSULEMODEL: + return CM_TempBoxModel( (const float*)VMA(1), (const float*)VMA(2), true ); + case CG_CM_POINTCONTENTS: + return CM_PointContents( (const float*)VMA(1), args[2] ); + case CG_CM_TRANSFORMEDPOINTCONTENTS: + return CM_TransformedPointContents( (const float*)VMA(1), (clipHandle_t)args[2], + (const float*)VMA(3), (const float*)VMA(4) ); + case CG_CM_BOXTRACE: + CM_BoxTrace( (trace_t*)VMA(1), (const float*)VMA(2), (const float*)VMA(3), + (float*)VMA(4), (float*)VMA(5), (clipHandle_t)args[6], args[7], TT_AABB ); + return 0; + case CG_CM_CAPSULETRACE: + CM_BoxTrace( (trace_t*)VMA(1), (const float*)VMA(2), (const float*)VMA(3), + (float*)VMA(4), (float*)VMA(5), (clipHandle_t)args[6], args[7], TT_CAPSULE ); + return 0; + case CG_CM_TRANSFORMEDBOXTRACE: + CM_TransformedBoxTrace( (trace_t*)VMA(1), (const float*)VMA(2), (const float*)VMA(3), + (float*)VMA(4), (float*)VMA(5), (clipHandle_t)args[6], args[7], + (const float*)VMA(8), (const float*)VMA(9), TT_AABB ); + return 0; + case CG_CM_TRANSFORMEDCAPSULETRACE: + CM_TransformedBoxTrace( (trace_t*)VMA(1), (const float*)VMA(2), (const float*)VMA(3), + (float*)VMA(4), (float*)VMA(5), (clipHandle_t)args[6], args[7], + (const float*)VMA(8), (const float*)VMA(9), TT_CAPSULE ); + return 0; + case CG_CM_BISPHERETRACE: + CM_BiSphereTrace( (trace_t*)VMA(1), (const float*)VMA(2), (const float*)VMA(3), VMF(4), VMF(5), (clipHandle_t)args[6], args[7] ); + return 0; + case CG_CM_TRANSFORMEDBISPHERETRACE: + CM_TransformedBiSphereTrace( (trace_t*)VMA(1), (const float*)VMA(2), (const float*)VMA(3), + VMF(4), VMF(5), (clipHandle_t)args[6], args[7], (const float*)VMA(8) ); + return 0; + case CG_CM_MARKFRAGMENTS: + { + float (&arg2)[3][3] = *reinterpret_cast<float (*)[3][3]>(VMA(2)); + return re.MarkFragments( args[1], arg2, (const float*)VMA(3), args[4], (float*)VMA(5), args[6], (markFragment_t*)VMA(7) ); + } + case CG_S_STARTSOUND: + S_StartSound( (float*)VMA(1), args[2], args[3], (sfxHandle_t)args[4] ); + return 0; + case CG_S_STARTLOCALSOUND: + S_StartLocalSound( (sfxHandle_t)args[1], args[2] ); + return 0; + case CG_S_CLEARLOOPINGSOUNDS: + S_ClearLoopingSounds( (bool)args[1] ); + return 0; + case CG_S_ADDLOOPINGSOUND: + S_AddLoopingSound( args[1], (const float*)VMA(2), (const float*)VMA(3), (sfxHandle_t)args[4] ); + return 0; + case CG_S_ADDREALLOOPINGSOUND: + S_AddRealLoopingSound( args[1], (const float*)VMA(2), (const float*)VMA(3), (sfxHandle_t)args[4] ); + return 0; + case CG_S_STOPLOOPINGSOUND: + S_StopLoopingSound( args[1] ); + return 0; + case CG_S_UPDATEENTITYPOSITION: + S_UpdateEntityPosition( args[1], (const float*)VMA(2) ); + return 0; + case CG_S_RESPATIALIZE: + { + float (&arg3)[3][3] = *reinterpret_cast<float (*)[3][3]>(VMA(3)); + S_Respatialize( args[1], (const float*)VMA(2), arg3, args[4] ); + return 0; + } + case CG_S_REGISTERSOUND: + return S_RegisterSound( (const char*)VMA(1), (bool)args[2] ); + case CG_S_SOUNDDURATION: + return S_SoundDuration( args[1] ); + case CG_S_STARTBACKGROUNDTRACK: + S_StartBackgroundTrack( (const char*)VMA(1), (const char*)VMA(2) ); + return 0; + case CG_R_LOADWORLDMAP: + re.LoadWorld( (const char*)VMA(1) ); + return 0; + case CG_R_REGISTERMODEL: + return re.RegisterModel( (const char*)VMA(1) ); + case CG_R_REGISTERSKIN: + return re.RegisterSkin( (const char*)VMA(1) ); + case CG_R_REGISTERSHADER: + return re.RegisterShader( (const char*)VMA(1) ); + case CG_R_REGISTERSHADERNOMIP: + return re.RegisterShaderNoMip( (const char*)VMA(1) ); + case CG_R_REGISTERFONT: + re.RegisterFont( (const char*)VMA(1), args[2], (fontInfo_t*)VMA(3)); + return 0; + case CG_R_CLEARSCENE: + re.ClearScene(); + return 0; + case CG_R_ADDREFENTITYTOSCENE: + re.AddRefEntityToScene( (const refEntity_t*)VMA(1) ); + return 0; + case CG_R_ADDPOLYTOSCENE: + re.AddPolyToScene( (qhandle_t)args[1], args[2], (const polyVert_t*)VMA(3), 1 ); + return 0; + case CG_R_ADDPOLYSTOSCENE: + re.AddPolyToScene( (qhandle_t)args[1], args[2], (const polyVert_t*)VMA(3), args[4] ); + return 0; + case CG_R_LIGHTFORPOINT: + return re.LightForPoint( (float*)VMA(1), (float*)VMA(2), (float*)VMA(3), (float*)VMA(4) ); + case CG_R_ADDLIGHTTOSCENE: + re.AddLightToScene( (const float*)VMA(1), VMF(2), VMF(3), VMF(4), VMF(5) ); + return 0; + case CG_R_ADDADDITIVELIGHTTOSCENE: + re.AddAdditiveLightToScene( (const float*)VMA(1), VMF(2), VMF(3), VMF(4), VMF(5) ); + return 0; + case CG_R_RENDERSCENE: + re.RenderScene( (const refdef_t*)VMA(1) ); + return 0; + case CG_R_SETCOLOR: + re.SetColor( (const float*)VMA(1) ); + return 0; + case CG_R_SETCLIPREGION: + re.SetClipRegion( (const float*)VMA(1) ); + return 0; + case CG_R_DRAWSTRETCHPIC: + re.DrawStretchPic( VMF(1), VMF(2), VMF(3), VMF(4), VMF(5), VMF(6), VMF(7), VMF(8), (qhandle_t)args[9] ); + return 0; + case CG_R_MODELBOUNDS: + re.ModelBounds( (qhandle_t)args[1], (float*)VMA(2), (float*)VMA(3) ); + return 0; + case CG_R_LERPTAG: + return re.LerpTag( (orientation_t*)VMA(1), args[2], args[3], args[4], VMF(5), (const char*)VMA(6) ); + case CG_GETGLCONFIG: + CL_GetGlconfig( (glconfig_t*)VMA(1) ); + return 0; + case CG_GETGAMESTATE: + CL_GetGameState( (gameState_t*)VMA(1) ); + return 0; + case CG_GETCURRENTSNAPSHOTNUMBER: + CL_GetCurrentSnapshotNumber( (int*)VMA(1), (int*)VMA(2) ); + return 0; + case CG_GETSNAPSHOT: + return CL_GetSnapshot( args[1], (snapshot_t*)VMA(2) ); + case CG_GETSERVERCOMMAND: + return CL_GetServerCommand( args[1] ); + case CG_GETCURRENTCMDNUMBER: + return CL_GetCurrentCmdNumber(); + case CG_GETUSERCMD: + return CL_GetUserCmd( args[1], (usercmd_t*)VMA(2) ); + case CG_SETUSERCMDVALUE: + CL_SetUserCmdValue( args[1], VMF(2) ); + return 0; + case CG_MEMORY_REMAINING: + return Hunk_MemoryRemaining(); + case CG_KEY_ISDOWN: + return Key_IsDown( args[1] ); + case CG_KEY_GETCATCHER: + return Key_GetCatcher(); + case CG_KEY_SETCATCHER: + // don't allow the cgame module to toggle the console + Key_SetCatcher( ( args[1] & ~KEYCATCH_CONSOLE ) | ( Key_GetCatcher() & KEYCATCH_CONSOLE ) ); + return 0; + case CG_KEY_GETKEY: + return Key_GetKey( (const char*)VMA(1) ); + + case CG_GETDEMOSTATE: + return CL_DemoState( ); + case CG_GETDEMOPOS: + return CL_DemoPos( ); + case CG_GETDEMONAME: + CL_DemoName( (char*)VMA(1), args[2] ); + return 0; + + case CG_KEY_KEYNUMTOSTRINGBUF: + Key_KeynumToStringBuf( args[1], (char*)VMA(2), args[3] ); + return 0; + case CG_KEY_GETBINDINGBUF: + Key_GetBindingBuf( args[1], (char*)VMA(2), args[3] ); + return 0; + case CG_KEY_SETBINDING: + Key_SetBinding( args[1], (const char*)VMA(2) ); + return 0; + + case CG_PARSE_ADD_GLOBAL_DEFINE: + return Parse_AddGlobalDefine( (char*)VMA(1) ); + case CG_PARSE_LOAD_SOURCE: + return Parse_LoadSourceHandle( (const char*)VMA(1) ); + case CG_PARSE_FREE_SOURCE: + return Parse_FreeSourceHandle( args[1] ); + case CG_PARSE_READ_TOKEN: + return Parse_ReadTokenHandle( args[1], (pc_token_t*)VMA(2) ); + case CG_PARSE_SOURCE_FILE_AND_LINE: + return Parse_SourceFileAndLine( args[1], (char*)VMA(2), (int*)VMA(3) ); + + case CG_KEY_SETOVERSTRIKEMODE: + Key_SetOverstrikeMode( (bool)args[1] ); + return 0; + + case CG_KEY_GETOVERSTRIKEMODE: + return Key_GetOverstrikeMode( ); + + case CG_FIELD_COMPLETELIST: + Field_CompleteList( (char*)VMA(1) ); + return 0; + + case CG_MEMSET: + ::memset( VMA(1), args[2], args[3] ); + return 0; + case CG_MEMCPY: + ::memcpy( VMA(1), VMA(2), args[3] ); + return 0; + case CG_STRNCPY: + safe_strncpy( (char*)VMA(1), (const char*)VMA(2), args[3] ); + return args[1]; + case CG_SIN: + return FloatAsInt( sin( VMF(1) ) ); + case CG_COS: + return FloatAsInt( cos( VMF(1) ) ); + case CG_ATAN2: + return FloatAsInt( atan2( VMF(1), VMF(2) ) ); + case CG_SQRT: + return FloatAsInt( sqrt( VMF(1) ) ); + case CG_FLOOR: + return FloatAsInt( floor( VMF(1) ) ); + case CG_CEIL: + return FloatAsInt( ceil( VMF(1) ) ); + case CG_ACOS: + return FloatAsInt( Q_acos( VMF(1) ) ); + + case CG_S_STOPBACKGROUNDTRACK: + S_StopBackgroundTrack(); + return 0; + + case CG_REAL_TIME: + return Com_RealTime( (qtime_t*)VMA(1) ); + case CG_SNAPVECTOR: + Q_SnapVector((float*)VMA(1)); + return 0; + + case CG_CIN_PLAYCINEMATIC: + return CIN_PlayCinematic((const char*)VMA(1), args[2], args[3], args[4], args[5], args[6]); + + case CG_CIN_STOPCINEMATIC: + return CIN_StopCinematic(args[1]); + + case CG_CIN_RUNCINEMATIC: + return CIN_RunCinematic(args[1]); + + case CG_CIN_DRAWCINEMATIC: + CIN_DrawCinematic(args[1]); + return 0; + + case CG_CIN_SETEXTENTS: + CIN_SetExtents(args[1], args[2], args[3], args[4], args[5]); + return 0; + + case CG_R_REMAP_SHADER: + re.RemapShader( (const char*)VMA(1), (const char*)VMA(2), (const char*)VMA(3) ); + return 0; + + case CG_GET_ENTITY_TOKEN: + return re.GetEntityToken( (char*)VMA(1), args[2] ); + case CG_R_INPVS: + return re.inPVS( (const float*)VMA(1), (const float*)VMA(2) ); + + default: + assert(0); + Com_Error( ERR_DROP, "Bad cgame system trap: %ld", (long int) args[0] ); + } + return 0; +} + + +/* +==================== +CL_InitCGame + +Should only be called by CL_StartHunkUsers +==================== +*/ +void CL_InitCGame( void ) { + const char *info; + const char *mapname; + int t1, t2; + char backup[ MAX_STRING_CHARS ]; + vmInterpret_t interpret; + + t1 = Sys_Milliseconds(); + + // find the current mapname + info = cl.gameState.stringData + cl.gameState.stringOffsets[ CS_SERVERINFO ]; + mapname = Info_ValueForKey( info, "mapname" ); + Com_sprintf( cl.mapname, sizeof( cl.mapname ), "maps/%s.bsp", mapname ); + + // load the dll or bytecode + interpret = (vmInterpret_t)Cvar_VariableValue("vm_cgame"); + if(cl_connectedToPureServer) + { + // if sv_pure is set we only allow qvms to be loaded + if(interpret != VMI_COMPILED && interpret != VMI_BYTECODE) + interpret = VMI_COMPILED; + } + + cls.cgame = VM_Create( "cgame", CL_CgameSystemCalls, interpret ); + if ( !cls.cgame ) { + Com_Error( ERR_DROP, "VM_Create on cgame failed" ); + } + clc.state = CA_LOADING; + + Cvar_VariableStringBuffer( "cl_voipSendTarget", backup, sizeof( backup ) ); + Cvar_Set( "cl_voipSendTarget", "" ); + + // Probe 1.1 or gpp cgame + cls.cgInterface = 0; + probingCG = true; + VM_Call( cls.cgame, CG_VOIP_STRING ); + probingCG = false; + + Cvar_Set( "cl_voipSendTarget", backup ); + + if ( ( clc.netchan.alternateProtocol == 2 ) != ( cls.cgInterface == 2 ) ) { + Com_Error( ERR_DROP, "%s protocol %i, but a cgame module using the %s interface was found", + ( clc.demoplaying ? "Demo was recorded using" : "Server uses" ), + ( clc.netchan.alternateProtocol == 0 ? PROTOCOL_VERSION : clc.netchan.alternateProtocol == 1 ? 70 : 69 ), + ( cls.cgInterface == 2 ? "1.1" : "non-1.1" ) ); + } + + // init for this gamestate + // use the lastExecutedServerCommand instead of the serverCommandSequence + // otherwise server commands sent just before a gamestate are dropped + VM_Call( cls.cgame, CG_INIT, clc.serverMessageSequence, clc.lastExecutedServerCommand, clc.clientNum ); + + // reset any CVAR_CHEAT cvars registered by cgame + if ( !clc.demoplaying && !cl_connectedToCheatServer ) + Cvar_SetCheatState(); + + // we will send a usercmd this frame, which + // will cause the server to send us the first snapshot + clc.state = CA_PRIMED; + + t2 = Sys_Milliseconds(); + + Com_Printf( "CL_InitCGame: %5.2f seconds\n", (t2-t1)/1000.0 ); + + // have the renderer touch all its images, so they are present + // on the card even if the driver does deferred loading + re.EndRegistration(); + + // make sure everything is paged in + if (!Sys_LowPhysicalMemory()) { + Com_TouchMemory(); + + CL_ProtocolSpecificCommandsInit(); + } + + // clear anything that got printed + Con_ClearNotify (); +} + + +/* +==================== +CL_GameCommand + +See if the current console command is claimed by the cgame +==================== +*/ +bool CL_GameCommand( void ) +{ + if ( !cls.cgame ) + return false; + + return (bool)VM_Call( cls.cgame, CG_CONSOLE_COMMAND ); +} + +/* +==================== +CL_GameConsoleText +==================== +*/ +void CL_GameConsoleText( void ) +{ + if ( !cls.cgame ) + return; + + VM_Call( cls.cgame, CG_CONSOLE_TEXT ); +} + +/* +===================== +CL_CGameRendering +===================== +*/ +void CL_CGameRendering( stereoFrame_t stereo ) +{ + VM_Call( cls.cgame, CG_DRAW_ACTIVE_FRAME, cl.serverTime, stereo, clc.demoplaying ); + VM_Debug( 0 ); +} + + +/* +================= +CL_AdjustTimeDelta + +Adjust the clients view of server time. + +We attempt to have cl.serverTime exactly equal the server's view +of time plus the timeNudge, but with variable latencies over +the internet it will often need to drift a bit to match conditions. + +Our ideal time would be to have the adjusted time approach, but not pass, +the very latest snapshot. + +Adjustments are only made when a new snapshot arrives with a rational +latency, which keeps the adjustment process framerate independent and +prevents massive overadjustment during times of significant packet loss +or bursted delayed packets. +================= +*/ + +#define RESET_TIME 500 + +void CL_AdjustTimeDelta( void ) { + int newDelta; + int deltaDelta; + + cl.newSnapshots = false; + + // the delta never drifts when replaying a demo + if ( clc.demoplaying ) { + return; + } + + newDelta = cl.snap.serverTime - cls.realtime; + deltaDelta = abs( newDelta - cl.serverTimeDelta ); + + if ( deltaDelta > RESET_TIME ) { + cl.serverTimeDelta = newDelta; + cl.oldServerTime = cl.snap.serverTime; // FIXME: is this a problem for cgame? + cl.serverTime = cl.snap.serverTime; + if ( cl_showTimeDelta->integer ) { + Com_Printf( "<RESET> " ); + } + } else if ( deltaDelta > 100 ) { + // fast adjust, cut the difference in half + if ( cl_showTimeDelta->integer ) { + Com_Printf( "<FAST> " ); + } + cl.serverTimeDelta = ( cl.serverTimeDelta + newDelta ) >> 1; + } else { + // slow drift adjust, only move 1 or 2 msec + + // if any of the frames between this and the previous snapshot + // had to be extrapolated, nudge our sense of time back a little + // the granularity of +1 / -2 is too high for timescale modified frametimes + if ( com_timescale->value == 0 || com_timescale->value == 1 ) { + if ( cl.extrapolatedSnapshot ) { + cl.extrapolatedSnapshot = false; + cl.serverTimeDelta -= 2; + } else { + // otherwise, move our sense of time forward to minimize total latency + cl.serverTimeDelta++; + } + } + } + + if ( cl_showTimeDelta->integer ) { + Com_Printf( "%i ", cl.serverTimeDelta ); + } +} + + +/* +================== +CL_FirstSnapshot +================== +*/ +void CL_FirstSnapshot( void ) { + // ignore snapshots that don't have entities + if ( cl.snap.snapFlags & SNAPFLAG_NOT_ACTIVE ) { + return; + } + clc.state = CA_ACTIVE; + + // set the timedelta so we are exactly on this first frame + cl.serverTimeDelta = cl.snap.serverTime - cls.realtime; + cl.oldServerTime = cl.snap.serverTime; + + clc.timeDemoBaseTime = cl.snap.serverTime; + + // if this is the first frame of active play, + // execute the contents of activeAction now + // this is to allow scripting a timedemo to start right + // after loading + if ( cl_activeAction->string[0] ) { + Cbuf_AddText( cl_activeAction->string ); + Cvar_Set( "activeAction", "" ); + } + +#ifdef USE_MUMBLE + if ((cl_useMumble->integer) && !mumble_islinked()) { + int ret = mumble_link(CLIENT_WINDOW_TITLE); + Com_Printf("Mumble: Linking to Mumble application %s\n", ret==0?"ok":"failed"); + } +#endif + +#ifdef USE_VOIP + if (!clc.voipCodecInitialized) { + int i; + int error; + + clc.opusEncoder = opus_encoder_create(48000, 1, OPUS_APPLICATION_VOIP, &error); + + if ( error ) { + Com_DPrintf("VoIP: Error opus_encoder_create %d\n", error); + return; + } + + for (i = 0; i < MAX_CLIENTS; i++) { + clc.opusDecoder[i] = opus_decoder_create(48000, 1, &error); + if ( error ) { + Com_DPrintf("VoIP: Error opus_decoder_create(%d) %d\n", i, error); + return; + } + clc.voipIgnore[i] = false; + clc.voipGain[i] = 1.0f; + } + clc.voipCodecInitialized = true; + clc.voipMuteAll = false; + Cmd_AddCommand ("voip", CL_Voip_f); + Cvar_Set("cl_voipSendTarget", "spatial"); + ::memset(clc.voipTargets, ~0, sizeof(clc.voipTargets)); + } +#endif +} + +/* +================== +CL_SetCGameTime +================== +*/ +void CL_SetCGameTime( void ) { + // getting a valid frame message ends the connection process + if ( clc.state != CA_ACTIVE ) { + if ( clc.state != CA_PRIMED ) { + return; + } + if ( clc.demoplaying ) { + // we shouldn't get the first snapshot on the same frame + // as the gamestate, because it causes a bad time skip + if ( !clc.firstDemoFrameSkipped ) { + clc.firstDemoFrameSkipped = true; + return; + } + CL_ReadDemoMessage(); + } + if ( cl.newSnapshots ) { + cl.newSnapshots = false; + CL_FirstSnapshot(); + } + if ( clc.state != CA_ACTIVE ) { + return; + } + } + + // if we have gotten to this point, cl.snap is guaranteed to be valid + if ( !cl.snap.valid ) { + Com_Error( ERR_DROP, "CL_SetCGameTime: !cl.snap.valid" ); + } + + // allow pause in single player + if ( sv_paused->integer && CL_CheckPaused() && com_sv_running->integer ) { + // paused + return; + } + + if ( cl.snap.serverTime < cl.oldFrameServerTime ) { + Com_Error( ERR_DROP, "cl.snap.serverTime < cl.oldFrameServerTime" ); + } + cl.oldFrameServerTime = cl.snap.serverTime; + + + // get our current view of time + + if ( clc.demoplaying && cl_freezeDemo->integer ) { + // cl_freezeDemo is used to lock a demo in place for single frame advances + + } else { + // cl_timeNudge is a user adjustable cvar that allows more + // or less latency to be added in the interest of better + // smoothness or better responsiveness. + int tn; + + tn = cl_timeNudge->integer; + if (tn<-30) { + tn = -30; + } else if (tn>30) { + tn = 30; + } + + cl.serverTime = cls.realtime + cl.serverTimeDelta - tn; + + // guarantee that time will never flow backwards, even if + // serverTimeDelta made an adjustment or cl_timeNudge was changed + if ( cl.serverTime < cl.oldServerTime ) { + cl.serverTime = cl.oldServerTime; + } + cl.oldServerTime = cl.serverTime; + + // note if we are almost past the latest frame (without timeNudge), + // so we will try and adjust back a bit when the next snapshot arrives + if ( cls.realtime + cl.serverTimeDelta >= cl.snap.serverTime - 5 ) { + cl.extrapolatedSnapshot = true; + } + } + + // if we have gotten new snapshots, drift serverTimeDelta + // don't do this every frame, or a period of packet loss would + // make a huge adjustment + if ( cl.newSnapshots ) { + CL_AdjustTimeDelta(); + } + + if ( !clc.demoplaying ) { + return; + } + + // if we are playing a demo back, we can just keep reading + // messages from the demo file until the cgame definately + // has valid snapshots to interpolate between + + // a timedemo will always use a deterministic set of time samples + // no matter what speed machine it is run on, + // while a normal demo may have different time samples + // each time it is played back + if ( cl_timedemo->integer ) { + int now = Sys_Milliseconds( ); + int frameDuration; + + if (!clc.timeDemoStart) { + clc.timeDemoStart = clc.timeDemoLastFrame = now; + clc.timeDemoMinDuration = INT_MAX; + clc.timeDemoMaxDuration = 0; + } + + frameDuration = now - clc.timeDemoLastFrame; + clc.timeDemoLastFrame = now; + + // Ignore the first measurement as it'll always be 0 + if( clc.timeDemoFrames > 0 ) + { + if( frameDuration > clc.timeDemoMaxDuration ) + clc.timeDemoMaxDuration = frameDuration; + + if( frameDuration < clc.timeDemoMinDuration ) + clc.timeDemoMinDuration = frameDuration; + + // 255 ms = about 4fps + if( frameDuration > UCHAR_MAX ) + frameDuration = UCHAR_MAX; + + clc.timeDemoDurations[ ( clc.timeDemoFrames - 1 ) % + MAX_TIMEDEMO_DURATIONS ] = frameDuration; + } + + clc.timeDemoFrames++; + cl.serverTime = clc.timeDemoBaseTime + clc.timeDemoFrames * 50; + } + + while ( cl.serverTime >= cl.snap.serverTime ) { + // feed another messag, which should change + // the contents of cl.snap + CL_ReadDemoMessage(); + if ( clc.state != CA_ACTIVE ) { + return; // end of demo + } + } +} diff --git a/src/client/cl_cin.cpp b/src/client/cl_cin.cpp new file mode 100644 index 0000000..6326e89 --- /dev/null +++ b/src/client/cl_cin.cpp @@ -0,0 +1,1937 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +/***************************************************************************** + * name: cl_cin.c + * + * desc: video and cinematic playback + * + * $Archive: /MissionPack/code/client/cl_cin.c $ + * + * cl_glconfig.hwtype trtypes 3dfx/ragepro need 256x256 + * + *****************************************************************************/ + +#include "client.h" +#include "snd_local.h" + +#define MAXSIZE 8 +#define MINSIZE 4 + +#define DEFAULT_CIN_WIDTH 512 +#define DEFAULT_CIN_HEIGHT 512 + +#define ROQ_QUAD 0x1000 +#define ROQ_QUAD_INFO 0x1001 +#define ROQ_CODEBOOK 0x1002 +#define ROQ_QUAD_VQ 0x1011 +#define ROQ_QUAD_JPEG 0x1012 +#define ROQ_QUAD_HANG 0x1013 +#define ROQ_PACKET 0x1030 +#define ZA_SOUND_MONO 0x1020 +#define ZA_SOUND_STEREO 0x1021 + +#define MAX_VIDEO_HANDLES 16 + +static void RoQ_init(void); + +/****************************************************************************** +* +* Class: trFMV +* +* Description: RoQ/RnR manipulation routines +* not entirely complete for first run +* +******************************************************************************/ + +static long ROQ_YY_tab[256]; +static long ROQ_UB_tab[256]; +static long ROQ_UG_tab[256]; +static long ROQ_VG_tab[256]; +static long ROQ_VR_tab[256]; +static unsigned short vq2[256 * 16 * 4]; +static unsigned short vq4[256 * 64 * 4]; +static unsigned short vq8[256 * 256 * 4]; + +struct cinematics_t +{ + byte linbuf[DEFAULT_CIN_WIDTH * DEFAULT_CIN_HEIGHT * 4 * 2]; + byte file[65536]; + short sqrTable[256]; + + int mcomp[256]; + byte *qStatus[2][32768]; + + long oldXOff; + long oldYOff; + long oldysize; + long oldxsize; + + int currentHandle; +}; + +struct cin_cache +{ + char fileName[MAX_OSPATH]; + + int CIN_WIDTH; + int CIN_HEIGHT; + + int xpos; + int ypos; + int width; + int height; + + bool looping; + bool holdAtEnd; + bool dirty; + bool alterGameState; + bool silent; + bool shader; + + fileHandle_t iFile; + e_status status; + + int startTime; + int lastTime; + long tfps; + long RoQPlayed; + long ROQSize; + unsigned int RoQFrameSize; + long onQuad; + long numQuads; + long samplesPerLine; + unsigned int roq_id; + long screenDelta; + + void (*VQ0)(byte *status, void *qdata); + void (*VQ1)(byte *status, void *qdata); + void (*VQNormal)(byte *status, void *qdata); + void (*VQBuffer)(byte *status, void *qdata); + + long samplesPerPixel; // defaults to 2 + byte *gray; + unsigned int xsize, ysize, maxsize, minsize; + + bool half; + bool smootheddouble; + bool inMemory; + long normalBuffer0; + long roq_flags; + long roqF0; + long roqF1; + long t[2]; + long roqFPS; + int playonwalls; + byte *buf; + long drawX; + long drawY; +}; + +static cinematics_t cin; +static cin_cache cinTable[MAX_VIDEO_HANDLES]; +static int currentHandle = -1; +static int CL_handle = -1; + +extern int s_soundtime; // sample PAIRS + +void CIN_CloseAllVideos(void) +{ + int i; + + for (i = 0; i < MAX_VIDEO_HANDLES; i++) + { + if (cinTable[i].fileName[0] != 0) + { + CIN_StopCinematic(i); + } + } +} + +static int CIN_HandleForVideo(void) +{ + int i; + + for (i = 0; i < MAX_VIDEO_HANDLES; i++) + { + if (cinTable[i].fileName[0] == 0) + { + return i; + } + } + Com_Error(ERR_DROP, "CIN_HandleForVideo: none free"); + return -1; +} + +//----------------------------------------------------------------------------- +// RllSetupTable +// +// Allocates and initializes the square table. +// +// Parameters: None +// +// Returns: Nothing +//----------------------------------------------------------------------------- +static void RllSetupTable(void) +{ + int z; + + for (z = 0; z < 128; z++) + { + cin.sqrTable[z] = (short)(z * z); + cin.sqrTable[z + 128] = (short)(-cin.sqrTable[z]); + } +} + +#if 0 +//----------------------------------------------------------------------------- +// RllDecodeMonoToMono +// +// Decode mono source data into a mono buffer. +// +// Parameters: from -> buffer holding encoded data +// to -> buffer to hold decoded data +// size = number of bytes of input (= # of shorts of output) +// signedOutput = 0 for unsigned output, non-zero for signed output +// flag = flags from asset header +// +// Returns: Number of samples placed in output buffer +//----------------------------------------------------------------------------- +long RllDecodeMonoToMono(unsigned char *from, short *to, unsigned int size, char signedOutput, unsigned short flag) +{ + unsigned int z; + int prev; + + if (signedOutput) + prev = flag - 0x8000; + else + prev = flag; + + for (z = 0; z < size; z++) + { + prev = to[z] = (short)(prev + cin.sqrTable[from[z]]); + } + return size; //*sizeof(short)); +} +#endif + +//----------------------------------------------------------------------------- +// RllDecodeMonoToStereo +// +// Decode mono source data into a stereo buffer. Output is 4 times the number +// of bytes in the input. +// +// Parameters: from -> buffer holding encoded data +// to -> buffer to hold decoded data +// size = number of bytes of input (= 1/4 # of bytes of output) +// signedOutput = 0 for unsigned output, non-zero for signed output +// flag = flags from asset header +// +// Returns: Number of samples placed in output buffer +//----------------------------------------------------------------------------- +static long RllDecodeMonoToStereo(unsigned char *from, short *to, unsigned int size, char signedOutput, unsigned short flag) +{ + unsigned int z; + int prev; + + if (signedOutput) + prev = flag - 0x8000; + else + prev = flag; + + for (z = 0; z < size; z++) + { + prev = (short)(prev + cin.sqrTable[from[z]]); + to[z * 2 + 0] = to[z * 2 + 1] = (short)(prev); + } + + return size; // * 2 * sizeof(short)); +} + +//----------------------------------------------------------------------------- +// RllDecodeStereoToStereo +// +// Decode stereo source data into a stereo buffer. +// +// Parameters: from -> buffer holding encoded data +// to -> buffer to hold decoded data +// size = number of bytes of input (= 1/2 # of bytes of output) +// signedOutput = 0 for unsigned output, non-zero for signed output +// flag = flags from asset header +// +// Returns: Number of samples placed in output buffer +//----------------------------------------------------------------------------- +static long RllDecodeStereoToStereo(unsigned char *from, short *to, unsigned int size, char signedOutput, unsigned short flag) +{ + unsigned int z; + unsigned char *zz = from; + int prevL, prevR; + + if (signedOutput) + { + prevL = (flag & 0xff00) - 0x8000; + prevR = ((flag & 0x00ff) << 8) - 0x8000; + } + else + { + prevL = flag & 0xff00; + prevR = (flag & 0x00ff) << 8; + } + + for (z = 0; z < size; z += 2) + { + prevL = (short)(prevL + cin.sqrTable[*zz++]); + prevR = (short)(prevR + cin.sqrTable[*zz++]); + to[z + 0] = (short)(prevL); + to[z + 1] = (short)(prevR); + } + + return (size >> 1); //*sizeof(short)); +} + +#if 0 +//----------------------------------------------------------------------------- +// RllDecodeStereoToMono +// +// Decode stereo source data into a mono buffer. +// +// Parameters: from -> buffer holding encoded data +// to -> buffer to hold decoded data +// size = number of bytes of input (= # of bytes of output) +// signedOutput = 0 for unsigned output, non-zero for signed output +// flag = flags from asset header +// +// Returns: Number of samples placed in output buffer +//----------------------------------------------------------------------------- +long RllDecodeStereoToMono(unsigned char *from, short *to, unsigned int size, char signedOutput, unsigned short flag) +{ + unsigned int z; + int prevL, prevR; + + if (signedOutput) + { + prevL = (flag & 0xff00) - 0x8000; + prevR = ((flag & 0x00ff) << 8) - 0x8000; + } + else + { + prevL = flag & 0xff00; + prevR = (flag & 0x00ff) << 8; + } + + for (z = 0; z < size; z += 1) + { + prevL = prevL + cin.sqrTable[from[z * 2]]; + prevR = prevR + cin.sqrTable[from[z * 2 + 1]]; + to[z] = (short)((prevL + prevR) / 2); + } + + return size; +} +#endif + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void move8_32(byte *src, byte *dst, int spl) +{ + int i; + + for (i = 0; i < 8; ++i) + { + memcpy(dst, src, 32); + src += spl; + dst += spl; + } +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void move4_32(byte *src, byte *dst, int spl) +{ + int i; + + for (i = 0; i < 4; ++i) + { + memcpy(dst, src, 16); + src += spl; + dst += spl; + } +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void blit8_32(byte *src, byte *dst, int spl) +{ + int i; + + for (i = 0; i < 8; ++i) + { + memcpy(dst, src, 32); + src += 32; + dst += spl; + } +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ +static void blit4_32(byte *src, byte *dst, int spl) +{ + int i; + + for (i = 0; i < 4; ++i) + { + memmove(dst, src, 16); + src += 16; + dst += spl; + } +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void blit2_32(byte *src, byte *dst, int spl) +{ + memcpy(dst, src, 8); + memcpy(dst + spl, src + 8, 8); +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void blitVQQuad32fs(byte **status, unsigned char *data) +{ + unsigned short newd, celdata, code; + unsigned int index, i; + int spl; + + newd = 0; + celdata = 0; + index = 0; + + spl = cinTable[currentHandle].samplesPerLine; + + do + { + if (!newd) + { + newd = 7; + celdata = data[0] + data[1] * 256; + data += 2; + } + else + { + newd--; + } + + code = (unsigned short)(celdata & 0xc000); + celdata <<= 2; + + switch (code) + { + case 0x8000: // vq code + blit8_32((byte *)&vq8[(*data) * 128], status[index], spl); + data++; + index += 5; + break; + case 0xc000: // drop + index++; // skip 8x8 + for (i = 0; i < 4; i++) + { + if (!newd) + { + newd = 7; + celdata = data[0] + data[1] * 256; + data += 2; + } + else + { + newd--; + } + + code = (unsigned short)(celdata & 0xc000); + celdata <<= 2; + + switch (code) + { // code in top two bits of code + case 0x8000: // 4x4 vq code + blit4_32((byte *)&vq4[(*data) * 32], status[index], spl); + data++; + break; + case 0xc000: // 2x2 vq code + blit2_32((byte *)&vq2[(*data) * 8], status[index], spl); + data++; + blit2_32((byte *)&vq2[(*data) * 8], status[index] + 8, spl); + data++; + blit2_32((byte *)&vq2[(*data) * 8], status[index] + spl * 2, spl); + data++; + blit2_32((byte *)&vq2[(*data) * 8], status[index] + spl * 2 + 8, spl); + data++; + break; + case 0x4000: // motion compensation + move4_32(status[index] + cin.mcomp[(*data)], status[index], spl); + data++; + break; + } + index++; + } + break; + case 0x4000: // motion compensation + move8_32(status[index] + cin.mcomp[(*data)], status[index], spl); + data++; + index += 5; + break; + case 0x0000: + index += 5; + break; + } + } while (status[index] != NULL); +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void ROQ_GenYUVTables(void) +{ + float t_ub, t_vr, t_ug, t_vg; + long i; + + t_ub = (1.77200f / 2.0f) * (float)(1 << 6) + 0.5f; + t_vr = (1.40200f / 2.0f) * (float)(1 << 6) + 0.5f; + t_ug = (0.34414f / 2.0f) * (float)(1 << 6) + 0.5f; + t_vg = (0.71414f / 2.0f) * (float)(1 << 6) + 0.5f; + for (i = 0; i < 256; i++) + { + float x = (float)(2 * i - 255); + + ROQ_UB_tab[i] = (long)((t_ub * x) + (1 << 5)); + ROQ_VR_tab[i] = (long)((t_vr * x) + (1 << 5)); + ROQ_UG_tab[i] = (long)((-t_ug * x)); + ROQ_VG_tab[i] = (long)((-t_vg * x) + (1 << 5)); + ROQ_YY_tab[i] = (long)((i << 6) | (i >> 2)); + } +} + +#define VQ2TO4(a, b, c, d) \ + { \ + *c++ = a[0]; \ + *d++ = a[0]; \ + *d++ = a[0]; \ + *c++ = a[1]; \ + *d++ = a[1]; \ + *d++ = a[1]; \ + *c++ = b[0]; \ + *d++ = b[0]; \ + *d++ = b[0]; \ + *c++ = b[1]; \ + *d++ = b[1]; \ + *d++ = b[1]; \ + *d++ = a[0]; \ + *d++ = a[0]; \ + *d++ = a[1]; \ + *d++ = a[1]; \ + *d++ = b[0]; \ + *d++ = b[0]; \ + *d++ = b[1]; \ + *d++ = b[1]; \ + a += 2; \ + b += 2; \ + } + +#define VQ2TO2(a, b, c, d) \ + { \ + *c++ = *a; \ + *d++ = *a; \ + *d++ = *a; \ + *c++ = *b; \ + *d++ = *b; \ + *d++ = *b; \ + *d++ = *a; \ + *d++ = *a; \ + *d++ = *b; \ + *d++ = *b; \ + a++; \ + b++; \ + } + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static unsigned short yuv_to_rgb(long y, long u, long v) +{ + long r, g, b, YY = (long)(ROQ_YY_tab[(y)]); + + r = (YY + ROQ_VR_tab[v]) >> 9; + g = (YY + ROQ_UG_tab[u] + ROQ_VG_tab[v]) >> 8; + b = (YY + ROQ_UB_tab[u]) >> 9; + + if (r < 0) r = 0; + if (g < 0) g = 0; + if (b < 0) b = 0; + if (r > 31) r = 31; + if (g > 63) g = 63; + if (b > 31) b = 31; + + return (unsigned short)((r << 11) + (g << 5) + (b)); +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ +static unsigned int yuv_to_rgb24(long y, long u, long v) +{ + long r, g, b, YY = (long)(ROQ_YY_tab[(y)]); + + r = (YY + ROQ_VR_tab[v]) >> 6; + g = (YY + ROQ_UG_tab[u] + ROQ_VG_tab[v]) >> 6; + b = (YY + ROQ_UB_tab[u]) >> 6; + + if (r < 0) r = 0; + if (g < 0) g = 0; + if (b < 0) b = 0; + if (r > 255) r = 255; + if (g > 255) g = 255; + if (b > 255) b = 255; + + return LittleLong((r) | (g << 8) | (b << 16) | (255 << 24)); +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void decodeCodeBook(byte *input, unsigned short roq_flags) +{ + long i, j, two, four; + unsigned short *aptr, *bptr, *cptr, *dptr; + long y0, y1, y2, y3, cr, cb; + byte *bbptr, *baptr, *bcptr, *bdptr; + union { + unsigned int *i; + unsigned short *s; + } iaptr, ibptr, icptr, idptr; + + if (!roq_flags) + { + two = four = 256; + } + else + { + two = roq_flags >> 8; + if (!two) two = 256; + four = roq_flags & 0xff; + } + + four *= 2; + + bptr = (unsigned short *)vq2; + + if (!cinTable[currentHandle].half) + { + if (!cinTable[currentHandle].smootheddouble) + { + // + // normal height + // + if (cinTable[currentHandle].samplesPerPixel == 2) + { + for (i = 0; i < two; i++) + { + y0 = (long)*input++; + y1 = (long)*input++; + y2 = (long)*input++; + y3 = (long)*input++; + cr = (long)*input++; + cb = (long)*input++; + *bptr++ = yuv_to_rgb(y0, cr, cb); + *bptr++ = yuv_to_rgb(y1, cr, cb); + *bptr++ = yuv_to_rgb(y2, cr, cb); + *bptr++ = yuv_to_rgb(y3, cr, cb); + } + + cptr = (unsigned short *)vq4; + dptr = (unsigned short *)vq8; + + for (i = 0; i < four; i++) + { + aptr = (unsigned short *)vq2 + (*input++) * 4; + bptr = (unsigned short *)vq2 + (*input++) * 4; + for (j = 0; j < 2; j++) VQ2TO4(aptr, bptr, cptr, dptr); + } + } + else if (cinTable[currentHandle].samplesPerPixel == 4) + { + ibptr.s = bptr; + for (i = 0; i < two; i++) + { + y0 = (long)*input++; + y1 = (long)*input++; + y2 = (long)*input++; + y3 = (long)*input++; + cr = (long)*input++; + cb = (long)*input++; + *ibptr.i++ = yuv_to_rgb24(y0, cr, cb); + *ibptr.i++ = yuv_to_rgb24(y1, cr, cb); + *ibptr.i++ = yuv_to_rgb24(y2, cr, cb); + *ibptr.i++ = yuv_to_rgb24(y3, cr, cb); + } + + icptr.s = vq4; + idptr.s = vq8; + + for (i = 0; i < four; i++) + { + iaptr.s = vq2; + iaptr.i += (*input++) * 4; + ibptr.s = vq2; + ibptr.i += (*input++) * 4; + for (j = 0; j < 2; j++) VQ2TO4(iaptr.i, ibptr.i, icptr.i, idptr.i); + } + } + else if (cinTable[currentHandle].samplesPerPixel == 1) + { + bbptr = (byte *)bptr; + for (i = 0; i < two; i++) + { + *bbptr++ = cinTable[currentHandle].gray[*input++]; + *bbptr++ = cinTable[currentHandle].gray[*input++]; + *bbptr++ = cinTable[currentHandle].gray[*input++]; + *bbptr++ = cinTable[currentHandle].gray[*input]; + input += 3; + } + + bcptr = (byte *)vq4; + bdptr = (byte *)vq8; + + for (i = 0; i < four; i++) + { + baptr = (byte *)vq2 + (*input++) * 4; + bbptr = (byte *)vq2 + (*input++) * 4; + for (j = 0; j < 2; j++) VQ2TO4(baptr, bbptr, bcptr, bdptr); + } + } + } + else + { + // + // double height, smoothed + // + if (cinTable[currentHandle].samplesPerPixel == 2) + { + for (i = 0; i < two; i++) + { + y0 = (long)*input++; + y1 = (long)*input++; + y2 = (long)*input++; + y3 = (long)*input++; + cr = (long)*input++; + cb = (long)*input++; + *bptr++ = yuv_to_rgb(y0, cr, cb); + *bptr++ = yuv_to_rgb(y1, cr, cb); + *bptr++ = yuv_to_rgb(((y0 * 3) + y2) / 4, cr, cb); + *bptr++ = yuv_to_rgb(((y1 * 3) + y3) / 4, cr, cb); + *bptr++ = yuv_to_rgb((y0 + (y2 * 3)) / 4, cr, cb); + *bptr++ = yuv_to_rgb((y1 + (y3 * 3)) / 4, cr, cb); + *bptr++ = yuv_to_rgb(y2, cr, cb); + *bptr++ = yuv_to_rgb(y3, cr, cb); + } + + cptr = (unsigned short *)vq4; + dptr = (unsigned short *)vq8; + + for (i = 0; i < four; i++) + { + aptr = (unsigned short *)vq2 + (*input++) * 8; + bptr = (unsigned short *)vq2 + (*input++) * 8; + for (j = 0; j < 2; j++) + { + VQ2TO4(aptr, bptr, cptr, dptr); + VQ2TO4(aptr, bptr, cptr, dptr); + } + } + } + else if (cinTable[currentHandle].samplesPerPixel == 4) + { + ibptr.s = bptr; + for (i = 0; i < two; i++) + { + y0 = (long)*input++; + y1 = (long)*input++; + y2 = (long)*input++; + y3 = (long)*input++; + cr = (long)*input++; + cb = (long)*input++; + *ibptr.i++ = yuv_to_rgb24(y0, cr, cb); + *ibptr.i++ = yuv_to_rgb24(y1, cr, cb); + *ibptr.i++ = yuv_to_rgb24(((y0 * 3) + y2) / 4, cr, cb); + *ibptr.i++ = yuv_to_rgb24(((y1 * 3) + y3) / 4, cr, cb); + *ibptr.i++ = yuv_to_rgb24((y0 + (y2 * 3)) / 4, cr, cb); + *ibptr.i++ = yuv_to_rgb24((y1 + (y3 * 3)) / 4, cr, cb); + *ibptr.i++ = yuv_to_rgb24(y2, cr, cb); + *ibptr.i++ = yuv_to_rgb24(y3, cr, cb); + } + + icptr.s = vq4; + idptr.s = vq8; + + for (i = 0; i < four; i++) + { + iaptr.s = vq2; + iaptr.i += (*input++) * 8; + ibptr.s = vq2; + ibptr.i += (*input++) * 8; + for (j = 0; j < 2; j++) + { + VQ2TO4(iaptr.i, ibptr.i, icptr.i, idptr.i); + VQ2TO4(iaptr.i, ibptr.i, icptr.i, idptr.i); + } + } + } + else if (cinTable[currentHandle].samplesPerPixel == 1) + { + bbptr = (byte *)bptr; + for (i = 0; i < two; i++) + { + y0 = (long)*input++; + y1 = (long)*input++; + y2 = (long)*input++; + y3 = (long)*input; + input += 3; + *bbptr++ = cinTable[currentHandle].gray[y0]; + *bbptr++ = cinTable[currentHandle].gray[y1]; + *bbptr++ = cinTable[currentHandle].gray[((y0 * 3) + y2) / 4]; + *bbptr++ = cinTable[currentHandle].gray[((y1 * 3) + y3) / 4]; + *bbptr++ = cinTable[currentHandle].gray[(y0 + (y2 * 3)) / 4]; + *bbptr++ = cinTable[currentHandle].gray[(y1 + (y3 * 3)) / 4]; + *bbptr++ = cinTable[currentHandle].gray[y2]; + *bbptr++ = cinTable[currentHandle].gray[y3]; + } + + bcptr = (byte *)vq4; + bdptr = (byte *)vq8; + + for (i = 0; i < four; i++) + { + baptr = (byte *)vq2 + (*input++) * 8; + bbptr = (byte *)vq2 + (*input++) * 8; + for (j = 0; j < 2; j++) + { + VQ2TO4(baptr, bbptr, bcptr, bdptr); + VQ2TO4(baptr, bbptr, bcptr, bdptr); + } + } + } + } + } + else + { + // + // 1/4 screen + // + if (cinTable[currentHandle].samplesPerPixel == 2) + { + for (i = 0; i < two; i++) + { + y0 = (long)*input; + input += 2; + y2 = (long)*input; + input += 2; + cr = (long)*input++; + cb = (long)*input++; + *bptr++ = yuv_to_rgb(y0, cr, cb); + *bptr++ = yuv_to_rgb(y2, cr, cb); + } + + cptr = (unsigned short *)vq4; + dptr = (unsigned short *)vq8; + + for (i = 0; i < four; i++) + { + aptr = (unsigned short *)vq2 + (*input++) * 2; + bptr = (unsigned short *)vq2 + (*input++) * 2; + for (j = 0; j < 2; j++) + { + VQ2TO2(aptr, bptr, cptr, dptr); + } + } + } + else if (cinTable[currentHandle].samplesPerPixel == 1) + { + bbptr = (byte *)bptr; + + for (i = 0; i < two; i++) + { + *bbptr++ = cinTable[currentHandle].gray[*input]; + input += 2; + *bbptr++ = cinTable[currentHandle].gray[*input]; + input += 4; + } + + bcptr = (byte *)vq4; + bdptr = (byte *)vq8; + + for (i = 0; i < four; i++) + { + baptr = (byte *)vq2 + (*input++) * 2; + bbptr = (byte *)vq2 + (*input++) * 2; + for (j = 0; j < 2; j++) + { + VQ2TO2(baptr, bbptr, bcptr, bdptr); + } + } + } + else if (cinTable[currentHandle].samplesPerPixel == 4) + { + ibptr.s = bptr; + for (i = 0; i < two; i++) + { + y0 = (long)*input; + input += 2; + y2 = (long)*input; + input += 2; + cr = (long)*input++; + cb = (long)*input++; + *ibptr.i++ = yuv_to_rgb24(y0, cr, cb); + *ibptr.i++ = yuv_to_rgb24(y2, cr, cb); + } + + icptr.s = vq4; + idptr.s = vq8; + + for (i = 0; i < four; i++) + { + iaptr.s = vq2; + iaptr.i += (*input++) * 2; + ibptr.s = vq2 + (*input++) * 2; + ibptr.i += (*input++) * 2; + for (j = 0; j < 2; j++) + { + VQ2TO2(iaptr.i, ibptr.i, icptr.i, idptr.i); + } + } + } + } +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void recurseQuad(long startX, long startY, long quadSize, long xOff, long yOff) +{ + byte *scroff; + long bigx, bigy, lowx, lowy, useY; + long offset; + + offset = cinTable[currentHandle].screenDelta; + + lowx = lowy = 0; + bigx = cinTable[currentHandle].xsize; + bigy = cinTable[currentHandle].ysize; + + if (bigx > cinTable[currentHandle].CIN_WIDTH) bigx = cinTable[currentHandle].CIN_WIDTH; + if (bigy > cinTable[currentHandle].CIN_HEIGHT) bigy = cinTable[currentHandle].CIN_HEIGHT; + + if ((startX >= lowx) && (startX + quadSize) <= (bigx) && (startY + quadSize) <= (bigy) && (startY >= lowy) && + quadSize <= MAXSIZE) + { + useY = startY; + scroff = cin.linbuf + + (useY + ((cinTable[currentHandle].CIN_HEIGHT - bigy) >> 1) + yOff) * + (cinTable[currentHandle].samplesPerLine) + + (((startX + xOff)) * cinTable[currentHandle].samplesPerPixel); + + cin.qStatus[0][cinTable[currentHandle].onQuad] = scroff; + cin.qStatus[1][cinTable[currentHandle].onQuad++] = scroff + offset; + } + + if (quadSize != MINSIZE) + { + quadSize >>= 1; + recurseQuad(startX, startY, quadSize, xOff, yOff); + recurseQuad(startX + quadSize, startY, quadSize, xOff, yOff); + recurseQuad(startX, startY + quadSize, quadSize, xOff, yOff); + recurseQuad(startX + quadSize, startY + quadSize, quadSize, xOff, yOff); + } +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void setupQuad(long xOff, long yOff) +{ + long numQuadCels, i, x, y; + byte *temp; + + if (xOff == cin.oldXOff && yOff == cin.oldYOff && cinTable[currentHandle].ysize == cin.oldysize && + cinTable[currentHandle].xsize == cin.oldxsize) + { + return; + } + + cin.oldXOff = xOff; + cin.oldYOff = yOff; + cin.oldysize = cinTable[currentHandle].ysize; + cin.oldxsize = cinTable[currentHandle].xsize; + + numQuadCels = (cinTable[currentHandle].xsize * cinTable[currentHandle].ysize) / (16); + numQuadCels += numQuadCels / 4; + numQuadCels += 64; // for overflow + + cinTable[currentHandle].onQuad = 0; + + for (y = 0; y < (long)cinTable[currentHandle].ysize; y += 16) + for (x = 0; x < (long)cinTable[currentHandle].xsize; x += 16) recurseQuad(x, y, 16, xOff, yOff); + + temp = NULL; + + for (i = (numQuadCels - 64); i < numQuadCels; i++) + { + cin.qStatus[0][i] = temp; // eoq + cin.qStatus[1][i] = temp; // eoq + } +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void readQuadInfo(byte *qData) +{ + if (currentHandle < 0) return; + + cinTable[currentHandle].xsize = qData[0] + qData[1] * 256; + cinTable[currentHandle].ysize = qData[2] + qData[3] * 256; + cinTable[currentHandle].maxsize = qData[4] + qData[5] * 256; + cinTable[currentHandle].minsize = qData[6] + qData[7] * 256; + + cinTable[currentHandle].CIN_HEIGHT = cinTable[currentHandle].ysize; + cinTable[currentHandle].CIN_WIDTH = cinTable[currentHandle].xsize; + + cinTable[currentHandle].samplesPerLine = + cinTable[currentHandle].CIN_WIDTH * cinTable[currentHandle].samplesPerPixel; + cinTable[currentHandle].screenDelta = cinTable[currentHandle].CIN_HEIGHT * cinTable[currentHandle].samplesPerLine; + + cinTable[currentHandle].half = false; + cinTable[currentHandle].smootheddouble = false; + + cinTable[currentHandle].VQ0 = cinTable[currentHandle].VQNormal; + cinTable[currentHandle].VQ1 = cinTable[currentHandle].VQBuffer; + + cinTable[currentHandle].t[0] = cinTable[currentHandle].screenDelta; + cinTable[currentHandle].t[1] = -cinTable[currentHandle].screenDelta; + + cinTable[currentHandle].drawX = cinTable[currentHandle].CIN_WIDTH; + cinTable[currentHandle].drawY = cinTable[currentHandle].CIN_HEIGHT; + + // rage pro is very slow at 512 wide textures, voodoo can't do it at all + if (cls.glconfig.hardwareType == GLHW_RAGEPRO || cls.glconfig.maxTextureSize <= 256) + { + if (cinTable[currentHandle].drawX > 256) + { + cinTable[currentHandle].drawX = 256; + } + if (cinTable[currentHandle].drawY > 256) + { + cinTable[currentHandle].drawY = 256; + } + if (cinTable[currentHandle].CIN_WIDTH != 256 || cinTable[currentHandle].CIN_HEIGHT != 256) + { + Com_Printf("HACK: approxmimating cinematic for Rage Pro or Voodoo\n"); + } + } +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void RoQPrepMcomp(long xoff, long yoff) +{ + long i, j, x, y, temp, temp2; + + i = cinTable[currentHandle].samplesPerLine; + j = cinTable[currentHandle].samplesPerPixel; + if (cinTable[currentHandle].xsize == (cinTable[currentHandle].ysize * 4) && !cinTable[currentHandle].half) + { + j = j + j; + i = i + i; + } + + for (y = 0; y < 16; y++) + { + temp2 = (y + yoff - 8) * i; + for (x = 0; x < 16; x++) + { + temp = (x + xoff - 8) * j; + cin.mcomp[(x * 16) + y] = cinTable[currentHandle].normalBuffer0 - (temp2 + temp); + } + } +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void initRoQ(void) +{ + if (currentHandle < 0) return; + + cinTable[currentHandle].VQNormal = (void (*)(byte *, void *))blitVQQuad32fs; + cinTable[currentHandle].VQBuffer = (void (*)(byte *, void *))blitVQQuad32fs; + cinTable[currentHandle].samplesPerPixel = 4; + ROQ_GenYUVTables(); + RllSetupTable(); +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ +/* +static byte* RoQFetchInterlaced( byte *source ) { + int x, *src, *dst; + + if (currentHandle < 0) return NULL; + + src = (int *)source; + dst = (int *)cinTable[currentHandle].buf2; + + for(x=0;x<256*256;x++) { + *dst = *src; + dst++; src += 2; + } + return cinTable[currentHandle].buf2; +} +*/ +static void RoQReset(void) +{ + if (currentHandle < 0) return; + + FS_FCloseFile(cinTable[currentHandle].iFile); + FS_FOpenFileRead(cinTable[currentHandle].fileName, &cinTable[currentHandle].iFile, true); + // let the background thread start reading ahead + FS_Read(cin.file, 16, cinTable[currentHandle].iFile); + RoQ_init(); + cinTable[currentHandle].status = FMV_LOOPED; +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void RoQInterrupt(void) +{ + byte *framedata; + short sbuf[32768]; + int ssize; + + if (currentHandle < 0) return; + + FS_Read(cin.file, cinTable[currentHandle].RoQFrameSize + 8, cinTable[currentHandle].iFile); + if (cinTable[currentHandle].RoQPlayed >= cinTable[currentHandle].ROQSize) + { + if (cinTable[currentHandle].holdAtEnd == false) + { + if (cinTable[currentHandle].looping) + { + RoQReset(); + } + else + { + cinTable[currentHandle].status = FMV_EOF; + } + } + else + { + cinTable[currentHandle].status = FMV_IDLE; + } + return; + } + + framedata = cin.file; +// +// new frame is ready +// +redump: + switch (cinTable[currentHandle].roq_id) + { + case ROQ_QUAD_VQ: + if ((cinTable[currentHandle].numQuads & 1)) + { + cinTable[currentHandle].normalBuffer0 = cinTable[currentHandle].t[1]; + RoQPrepMcomp(cinTable[currentHandle].roqF0, cinTable[currentHandle].roqF1); + cinTable[currentHandle].VQ1((byte *)cin.qStatus[1], framedata); + cinTable[currentHandle].buf = cin.linbuf + cinTable[currentHandle].screenDelta; + } + else + { + cinTable[currentHandle].normalBuffer0 = cinTable[currentHandle].t[0]; + RoQPrepMcomp(cinTable[currentHandle].roqF0, cinTable[currentHandle].roqF1); + cinTable[currentHandle].VQ0((byte *)cin.qStatus[0], framedata); + cinTable[currentHandle].buf = cin.linbuf; + } + if (cinTable[currentHandle].numQuads == 0) + { // first frame + ::memcpy(cin.linbuf + cinTable[currentHandle].screenDelta, cin.linbuf, + cinTable[currentHandle].samplesPerLine * cinTable[currentHandle].ysize); + } + cinTable[currentHandle].numQuads++; + cinTable[currentHandle].dirty = true; + break; + case ROQ_CODEBOOK: + decodeCodeBook(framedata, (unsigned short)cinTable[currentHandle].roq_flags); + break; + case ZA_SOUND_MONO: + if (!cinTable[currentHandle].silent) + { + ssize = RllDecodeMonoToStereo(framedata, sbuf, cinTable[currentHandle].RoQFrameSize, 0, + (unsigned short)cinTable[currentHandle].roq_flags); + S_RawSamples(0, ssize, 22050, 2, 1, (byte *)sbuf, 1.0f, -1); + } + break; + case ZA_SOUND_STEREO: + if (!cinTable[currentHandle].silent) + { + if (cinTable[currentHandle].numQuads == -1) + { + S_Update(); + s_rawend[0] = s_soundtime; + } + ssize = RllDecodeStereoToStereo(framedata, sbuf, cinTable[currentHandle].RoQFrameSize, 0, + (unsigned short)cinTable[currentHandle].roq_flags); + S_RawSamples(0, ssize, 22050, 2, 2, (byte *)sbuf, 1.0f, -1); + } + break; + case ROQ_QUAD_INFO: + if (cinTable[currentHandle].numQuads == -1) + { + readQuadInfo(framedata); + setupQuad(0, 0); + cinTable[currentHandle].startTime = cinTable[currentHandle].lastTime = CL_ScaledMilliseconds(); + } + if (cinTable[currentHandle].numQuads != 1) cinTable[currentHandle].numQuads = 0; + break; + case ROQ_PACKET: + cinTable[currentHandle].inMemory = (bool)cinTable[currentHandle].roq_flags; + cinTable[currentHandle].RoQFrameSize = 0; // for header + break; + case ROQ_QUAD_HANG: + cinTable[currentHandle].RoQFrameSize = 0; + break; + case ROQ_QUAD_JPEG: + break; + default: + cinTable[currentHandle].status = FMV_EOF; + break; + } + // + // read in next frame data + // + if (cinTable[currentHandle].RoQPlayed >= cinTable[currentHandle].ROQSize) + { + if (cinTable[currentHandle].holdAtEnd == false) + { + if (cinTable[currentHandle].looping) + { + RoQReset(); + } + else + { + cinTable[currentHandle].status = FMV_EOF; + } + } + else + { + cinTable[currentHandle].status = FMV_IDLE; + } + return; + } + + framedata += cinTable[currentHandle].RoQFrameSize; + cinTable[currentHandle].roq_id = framedata[0] + framedata[1] * 256; + cinTable[currentHandle].RoQFrameSize = framedata[2] + framedata[3] * 256 + framedata[4] * 65536; + cinTable[currentHandle].roq_flags = framedata[6] + framedata[7] * 256; + cinTable[currentHandle].roqF0 = (signed char)framedata[7]; + cinTable[currentHandle].roqF1 = (signed char)framedata[6]; + + if (cinTable[currentHandle].RoQFrameSize > 65536 || cinTable[currentHandle].roq_id == 0x1084) + { + Com_DPrintf("roq_size>65536||roq_id==0x1084\n"); + cinTable[currentHandle].status = FMV_EOF; + if (cinTable[currentHandle].looping) + { + RoQReset(); + } + return; + } + if (cinTable[currentHandle].inMemory && (cinTable[currentHandle].status != FMV_EOF)) + { + cinTable[currentHandle].inMemory = false; + framedata += 8; + goto redump; + } + // + // one more frame hits the dust + // + // assert(cinTable[currentHandle].RoQFrameSize <= 65536); + // r = FS_Read( cin.file, cinTable[currentHandle].RoQFrameSize+8, cinTable[currentHandle].iFile ); + cinTable[currentHandle].RoQPlayed += cinTable[currentHandle].RoQFrameSize + 8; +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void RoQ_init(void) +{ + cinTable[currentHandle].startTime = cinTable[currentHandle].lastTime = CL_ScaledMilliseconds(); + + cinTable[currentHandle].RoQPlayed = 24; + + /* get frame rate */ + cinTable[currentHandle].roqFPS = cin.file[6] + cin.file[7] * 256; + + if (!cinTable[currentHandle].roqFPS) cinTable[currentHandle].roqFPS = 30; + + cinTable[currentHandle].numQuads = -1; + + cinTable[currentHandle].roq_id = cin.file[8] + cin.file[9] * 256; + cinTable[currentHandle].RoQFrameSize = cin.file[10] + cin.file[11] * 256 + cin.file[12] * 65536; + cinTable[currentHandle].roq_flags = cin.file[14] + cin.file[15] * 256; + + if (cinTable[currentHandle].RoQFrameSize > 65536 || !cinTable[currentHandle].RoQFrameSize) + { + return; + } +} + +/****************************************************************************** +* +* Function: +* +* Description: +* +******************************************************************************/ + +static void RoQShutdown(void) +{ + const char *s; + + if (!cinTable[currentHandle].buf) + { + return; + } + + if (cinTable[currentHandle].status == FMV_IDLE) + { + return; + } + Com_DPrintf("finished cinematic\n"); + cinTable[currentHandle].status = FMV_IDLE; + + if (cinTable[currentHandle].iFile) + { + FS_FCloseFile(cinTable[currentHandle].iFile); + cinTable[currentHandle].iFile = 0; + } + + if (cinTable[currentHandle].alterGameState) + { + clc.state = CA_DISCONNECTED; + // we can't just do a vstr nextmap, because + // if we are aborting the intro cinematic with + // a devmap command, nextmap would be valid by + // the time it was referenced + s = Cvar_VariableString("nextmap"); + if (s[0]) + { + Cbuf_ExecuteText(EXEC_APPEND, va("%s\n", s)); + Cvar_Set("nextmap", ""); + } + CL_handle = -1; + + CL_ProtocolSpecificCommandsInit(); + } + cinTable[currentHandle].fileName[0] = 0; + currentHandle = -1; +} + +/* +================== +CIN_StopCinematic +================== +*/ +e_status CIN_StopCinematic(int handle) +{ + if (handle < 0 || handle >= MAX_VIDEO_HANDLES || cinTable[handle].status == FMV_EOF) return FMV_EOF; + currentHandle = handle; + + Com_DPrintf("trFMV::stop(), closing %s\n", cinTable[currentHandle].fileName); + + if (!cinTable[currentHandle].buf) + { + return FMV_EOF; + } + + if (cinTable[currentHandle].alterGameState) + { + if (clc.state != CA_CINEMATIC) + { + return cinTable[currentHandle].status; + } + } + cinTable[currentHandle].status = FMV_EOF; + RoQShutdown(); + + return FMV_EOF; +} + +/* +================== +CIN_RunCinematic + +Fetch and decompress the pending frame +================== +*/ + +e_status CIN_RunCinematic(int handle) +{ + int start = 0; + int thisTime = 0; + + if (handle < 0 || handle >= MAX_VIDEO_HANDLES || cinTable[handle].status == FMV_EOF) return FMV_EOF; + + if (cin.currentHandle != handle) + { + currentHandle = handle; + cin.currentHandle = currentHandle; + cinTable[currentHandle].status = FMV_EOF; + RoQReset(); + } + + if (cinTable[handle].playonwalls < -1) + { + return cinTable[handle].status; + } + + currentHandle = handle; + + if (cinTable[currentHandle].alterGameState) + { + if (clc.state != CA_CINEMATIC) + { + return cinTable[currentHandle].status; + } + } + + if (cinTable[currentHandle].status == FMV_IDLE) + { + return cinTable[currentHandle].status; + } + + thisTime = CL_ScaledMilliseconds(); + if (cinTable[currentHandle].shader && (abs(thisTime - cinTable[currentHandle].lastTime)) > 100) + { + cinTable[currentHandle].startTime += thisTime - cinTable[currentHandle].lastTime; + } + cinTable[currentHandle].tfps = (((CL_ScaledMilliseconds() - cinTable[currentHandle].startTime) * 3) / 100); + + start = cinTable[currentHandle].startTime; + while ((cinTable[currentHandle].tfps != cinTable[currentHandle].numQuads) && + (cinTable[currentHandle].status == FMV_PLAY)) + { + RoQInterrupt(); + if (start != cinTable[currentHandle].startTime) + { + cinTable[currentHandle].tfps = (((CL_ScaledMilliseconds() - cinTable[currentHandle].startTime) * 3) / 100); + start = cinTable[currentHandle].startTime; + } + } + + cinTable[currentHandle].lastTime = thisTime; + + if (cinTable[currentHandle].status == FMV_LOOPED) + { + cinTable[currentHandle].status = FMV_PLAY; + } + + if (cinTable[currentHandle].status == FMV_EOF) + { + if (cinTable[currentHandle].looping) + { + RoQReset(); + } + else + { + RoQShutdown(); + return FMV_EOF; + } + } + + return cinTable[currentHandle].status; +} + +void CIN_SetExtents(int handle, int x, int y, int w, int h) +{ + if (handle < 0 || handle >= MAX_VIDEO_HANDLES || cinTable[handle].status == FMV_EOF) return; + cinTable[handle].xpos = x; + cinTable[handle].ypos = y; + cinTable[handle].width = w; + cinTable[handle].height = h; + cinTable[handle].dirty = true; +} + +static void CIN_SetLooping(int handle, bool loop) +{ + if (handle < 0 || handle >= MAX_VIDEO_HANDLES || cinTable[handle].status == FMV_EOF) return; + cinTable[handle].looping = loop; +} + +/* +================== +CIN_PlayCinematic +================== +*/ +int CIN_PlayCinematic(const char *arg, int x, int y, int w, int h, int systemBits) +{ + unsigned short RoQID; + char name[MAX_OSPATH]; + int i; + + if (strstr(arg, "/") == NULL && strstr(arg, "\\") == NULL) + { + Com_sprintf(name, sizeof(name), "video/%s", arg); + } + else + { + Com_sprintf(name, sizeof(name), "%s", arg); + } + + if (!(systemBits & CIN_system)) + { + for (i = 0; i < MAX_VIDEO_HANDLES; i++) + { + if (!strcmp(cinTable[i].fileName, name)) + { + return i; + } + } + } + + Com_DPrintf("CIN_PlayCinematic( %s )\n", arg); + + ::memset(&cin, 0, sizeof(cinematics_t)); + currentHandle = CIN_HandleForVideo(); + + cin.currentHandle = currentHandle; + + strcpy(cinTable[currentHandle].fileName, name); + + cinTable[currentHandle].ROQSize = 0; + cinTable[currentHandle].ROQSize = + FS_FOpenFileRead(cinTable[currentHandle].fileName, &cinTable[currentHandle].iFile, true); + + if (cinTable[currentHandle].ROQSize <= 0) + { + Com_DPrintf("play(%s), ROQSize<=0\n", arg); + cinTable[currentHandle].fileName[0] = 0; + return -1; + } + + CIN_SetExtents(currentHandle, x, y, w, h); + CIN_SetLooping(currentHandle, ((systemBits & CIN_loop) != 0)); + + cinTable[currentHandle].CIN_HEIGHT = DEFAULT_CIN_HEIGHT; + cinTable[currentHandle].CIN_WIDTH = DEFAULT_CIN_WIDTH; + cinTable[currentHandle].holdAtEnd = (systemBits & CIN_hold) != 0; + cinTable[currentHandle].alterGameState = (systemBits & CIN_system) != 0; + cinTable[currentHandle].playonwalls = 1; + cinTable[currentHandle].silent = (systemBits & CIN_silent) != 0; + cinTable[currentHandle].shader = (systemBits & CIN_shader) != 0; + + if (cinTable[currentHandle].alterGameState) + { + // close the menu + if (cls.ui) + { + VM_Call(cls.ui, UI_SET_ACTIVE_MENU - cls.uiInterface == 2 ? 2 : 0, UIMENU_NONE); + } + } + else + { + cinTable[currentHandle].playonwalls = cl_inGameVideo->integer; + } + + initRoQ(); + + FS_Read(cin.file, 16, cinTable[currentHandle].iFile); + + RoQID = (unsigned short)(cin.file[0]) + (unsigned short)(cin.file[1]) * 256; + if (RoQID == 0x1084) + { + RoQ_init(); + // FS_Read (cin.file, cinTable[currentHandle].RoQFrameSize+8, cinTable[currentHandle].iFile); + + cinTable[currentHandle].status = FMV_PLAY; + Com_DPrintf("trFMV::play(), playing %s\n", arg); + + if (cinTable[currentHandle].alterGameState) + { + clc.state = CA_CINEMATIC; + } + + if (!cinTable[currentHandle].silent) + { + s_rawend[0] = s_soundtime; + } + + return currentHandle; + } + Com_DPrintf("trFMV::play(), invalid RoQ ID\n"); + + RoQShutdown(); + return -1; +} + +/* +================== +CIN_ResampleCinematic + +Resample cinematic to 256x256 and store in buf2 +================== +*/ +static void CIN_ResampleCinematic(int handle, int *buf2) +{ + int ix, iy, *buf3, xm, ym, ll; + byte *buf; + + buf = cinTable[handle].buf; + + xm = cinTable[handle].CIN_WIDTH / 256; + ym = cinTable[handle].CIN_HEIGHT / 256; + ll = 8; + if (cinTable[handle].CIN_WIDTH == 512) + { + ll = 9; + } + + buf3 = (int *)buf; + if (xm == 2 && ym == 2) + { + byte *bc2, *bc3; + int ic, iiy; + + bc2 = (byte *)buf2; + bc3 = (byte *)buf3; + for (iy = 0; iy < 256; iy++) + { + iiy = iy << 12; + for (ix = 0; ix < 2048; ix += 8) + { + for (ic = ix; ic < (ix + 4); ic++) + { + *bc2 = (bc3[iiy + ic] + bc3[iiy + 4 + ic] + bc3[iiy + 2048 + ic] + bc3[iiy + 2048 + 4 + ic]) >> 2; + bc2++; + } + } + } + } + else if (xm == 2 && ym == 1) + { + byte *bc2, *bc3; + int ic, iiy; + + bc2 = (byte *)buf2; + bc3 = (byte *)buf3; + for (iy = 0; iy < 256; iy++) + { + iiy = iy << 11; + for (ix = 0; ix < 2048; ix += 8) + { + for (ic = ix; ic < (ix + 4); ic++) + { + *bc2 = (bc3[iiy + ic] + bc3[iiy + 4 + ic]) >> 1; + bc2++; + } + } + } + } + else + { + for (iy = 0; iy < 256; iy++) + { + for (ix = 0; ix < 256; ix++) + { + buf2[(iy << 8) + ix] = buf3[((iy * ym) << ll) + (ix * xm)]; + } + } + } +} + +/* +================== +CIN_DrawCinematic +================== +*/ +void CIN_DrawCinematic(int handle) +{ + float x, y, w, h; + byte *buf; + + if (handle < 0 || handle >= MAX_VIDEO_HANDLES || cinTable[handle].status == FMV_EOF) return; + + if (!cinTable[handle].buf) + { + return; + } + + x = cinTable[handle].xpos; + y = cinTable[handle].ypos; + w = cinTable[handle].width; + h = cinTable[handle].height; + buf = cinTable[handle].buf; + SCR_AdjustFrom640(&x, &y, &w, &h); + + if (cinTable[handle].dirty && + (cinTable[handle].CIN_WIDTH != cinTable[handle].drawX + || cinTable[handle].CIN_HEIGHT != cinTable[handle].drawY)) + { + int *buf2 = (int *)Hunk_AllocateTempMemory(256 * 256 * 4); + + CIN_ResampleCinematic(handle, buf2); + + re.DrawStretchRaw(x, y, w, h, 256, 256, (byte *)buf2, handle, true); + cinTable[handle].dirty = false; + Hunk_FreeTempMemory(buf2); + return; + } + + re.DrawStretchRaw(x, y, w, h, cinTable[handle].drawX, cinTable[handle].drawY, + buf, handle, cinTable[handle].dirty); + + cinTable[handle].dirty = false; +} + +void CL_PlayCinematic_f(void) +{ + int bits = CIN_system; + + Com_DPrintf("CL_PlayCinematic_f\n"); + if (clc.state == CA_CINEMATIC) + { + SCR_StopCinematic(); + } + + const char *arg = Cmd_Argv(1); + const char *s = Cmd_Argv(2); + + if ((s && s[0] == '1') || Q_stricmp(arg, "demoend.roq") == 0 || Q_stricmp(arg, "end.roq") == 0) + { + bits |= CIN_hold; + } + if (s && s[0] == '2') + { + bits |= CIN_loop; + } + + S_StopAllSounds(); + + CL_handle = CIN_PlayCinematic(arg, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, bits); + if (CL_handle >= 0) + { + do + { + SCR_RunCinematic(); + } while (cinTable[currentHandle].buf == NULL && + cinTable[currentHandle].status == FMV_PLAY); // wait for first frame (load codebook and sound) + } +} + +void SCR_DrawCinematic(void) +{ + if (CL_handle >= 0 && CL_handle < MAX_VIDEO_HANDLES) + { + CIN_DrawCinematic(CL_handle); + } +} + +void SCR_RunCinematic(void) +{ + if (CL_handle >= 0 && CL_handle < MAX_VIDEO_HANDLES) + { + CIN_RunCinematic(CL_handle); + } +} + +void SCR_StopCinematic(void) +{ + if (CL_handle >= 0 && CL_handle < MAX_VIDEO_HANDLES) + { + CIN_StopCinematic(CL_handle); + S_StopAllSounds(); + CL_handle = -1; + } +} + +void CIN_UploadCinematic(int handle) +{ + if (handle >= 0 && handle < MAX_VIDEO_HANDLES) + { + if (!cinTable[handle].buf) + { + return; + } + if (cinTable[handle].playonwalls <= 0 && cinTable[handle].dirty) + { + if (cinTable[handle].playonwalls == 0) + { + cinTable[handle].playonwalls = -1; + } + else + { + if (cinTable[handle].playonwalls == -1) + { + cinTable[handle].playonwalls = -2; + } + else + { + cinTable[handle].dirty = false; + } + } + } + + // Resample the video if needed + if (cinTable[handle].dirty && (cinTable[handle].CIN_WIDTH != cinTable[handle].drawX || + cinTable[handle].CIN_HEIGHT != cinTable[handle].drawY)) + { + int *buf2 = (int *)Hunk_AllocateTempMemory(256 * 256 * 4); + + CIN_ResampleCinematic(handle, buf2); + + re.UploadCinematic( + cinTable[handle].CIN_WIDTH, cinTable[handle].CIN_HEIGHT, 256, 256, (byte *)buf2, handle, true); + cinTable[handle].dirty = false; + Hunk_FreeTempMemory(buf2); + } + else + { + // Upload video at normal resolution + re.UploadCinematic(cinTable[handle].CIN_WIDTH, cinTable[handle].CIN_HEIGHT, cinTable[handle].drawX, + cinTable[handle].drawY, cinTable[handle].buf, handle, cinTable[handle].dirty); + cinTable[handle].dirty = false; + } + + if (cl_inGameVideo->integer == 0 && cinTable[handle].playonwalls == 1) + { + cinTable[handle].playonwalls--; + } + else if (cl_inGameVideo->integer != 0 && cinTable[handle].playonwalls != 1) + { + cinTable[handle].playonwalls = 1; + } + } +} diff --git a/src/client/cl_console.cpp b/src/client/cl_console.cpp new file mode 100644 index 0000000..e3e928c --- /dev/null +++ b/src/client/cl_console.cpp @@ -0,0 +1,877 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ +// console.c + +#include "client.h" + +#include "qcommon/cdefs.h" + +int g_console_field_width = 78; + + +#define NUM_CON_TIMES 4 + +#define CON_TEXTSIZE 163840 +typedef struct { + bool initialized; + + short text[CON_TEXTSIZE]; + int current; // line where next message will be printed + int x; // offset in current line for next print + int display; // bottom of console displays this line + + int linewidth; // characters across screen + int totallines; // total lines in console scrollback + + float xadjust; // for wide aspect screens + + float displayFrac; // aproaches finalFrac at scr_conspeed + float finalFrac; // 0.0 to 1.0 lines of console to display + + int vislines; // in scanlines + + vec4_t color; +} console_t; + +console_t con; + +cvar_t *con_conspeed; +cvar_t *con_height; +cvar_t *con_useShader; +cvar_t *con_colorRed; +cvar_t *con_colorGreen; +cvar_t *con_colorBlue; +cvar_t *con_colorAlpha; +cvar_t *con_versionStr; + +#define DEFAULT_CONSOLE_WIDTH 78 + + +/* +================ +Con_ToggleConsole_f +================ +*/ +void Con_ToggleConsole_f (void) { + // Can't toggle the console when it's the only thing available + if ( clc.state == CA_DISCONNECTED && Key_GetCatcher( ) == KEYCATCH_CONSOLE ) { + return; + } + + Field_Clear( &g_consoleField ); + g_consoleField.widthInChars = g_console_field_width; + + Key_SetCatcher( Key_GetCatcher( ) ^ KEYCATCH_CONSOLE ); +} + +/* +=================== +Con_ToggleMenu_f +=================== +*/ +void Con_ToggleMenu_f( void ) { + CL_KeyEvent( K_ESCAPE, true, Sys_Milliseconds() ); + CL_KeyEvent( K_ESCAPE, false, Sys_Milliseconds() ); +} + +/* +=================== +Con_MessageMode_f +=================== +-*/ +void Con_MessageMode_f (void) { + chat_playerNum = -1; + chat_team = false; + chat_admins = false; + chat_clans = false; + Field_Clear( &chatField ); + chatField.widthInChars = 30; + + Key_SetCatcher( Key_GetCatcher( ) ^ KEYCATCH_MESSAGE ); +} + +/* +==================== +Con_MessageMode2_f +==================== +*/ +void Con_MessageMode2_f (void) { + chat_playerNum = -1; + chat_team = true; + chat_admins = false; + chat_clans = false; + Field_Clear( &chatField ); + chatField.widthInChars = 25; + Key_SetCatcher( Key_GetCatcher( ) ^ KEYCATCH_MESSAGE ); +} + +/* +=================== +Con_MessageMode3_f +=================== +*/ +void Con_MessageMode3_f (void) { + chat_playerNum = VM_Call( cls.cgame, CG_CROSSHAIR_PLAYER ); + if ( chat_playerNum < 0 || chat_playerNum >= MAX_CLIENTS ) { + chat_playerNum = -1; + return; + } + chat_team = false; + chat_admins = false; + chat_clans = false; + Field_Clear( &chatField ); + chatField.widthInChars = 30; + Key_SetCatcher( Key_GetCatcher( ) ^ KEYCATCH_MESSAGE ); +} + +/* +===================== +Con_MessageMode4_f +===================== +*/ +void Con_MessageMode4_f (void) { + chat_playerNum = VM_Call( cls.cgame, CG_LAST_ATTACKER ); + if ( chat_playerNum < 0 || chat_playerNum >= MAX_CLIENTS ) { + chat_playerNum = -1; + return; + } + chat_team = false; + chat_admins = false; + chat_clans = false; + Field_Clear( &chatField ); + chatField.widthInChars = 30; + Key_SetCatcher( Key_GetCatcher( ) ^ KEYCATCH_MESSAGE ); +} + +/* +================ +Con_MessageMode5_f +================ +*/ +void Con_MessageMode5_f (void) { + int i; + chat_playerNum = -1; + chat_team = false; + chat_admins = true; + chat_clans = false; + Field_Clear( &chatField ); + chatField.widthInChars = 25; + Key_SetCatcher( Key_GetCatcher( ) ^ KEYCATCH_MESSAGE ); +} + +/* +================ +Con_MessageMode6_f +================ +*/ +void Con_MessageMode6_f (void) { + int i; + chat_playerNum = -1; + chat_team = false; + chat_admins = false; + chat_clans = true; + Field_Clear( &chatField ); + chatField.widthInChars = 25; + Key_SetCatcher( Key_GetCatcher( ) ^ KEYCATCH_MESSAGE ); +} + +/* +================ +Con_Clear_f +================ +*/ +void Con_Clear_f (void) { + int i; + + for ( i = 0 ; i < CON_TEXTSIZE ; i++ ) { + con.text[i] = (ColorIndex(COLOR_WHITE)<<8) | ' '; + } + + Con_Bottom(); // go to end +} + + +/* +================ +Con_Dump_f + +Save the console contents out to a file +================ +*/ +void Con_Dump_f (void) +{ + int l, x, i; + short *line; + fileHandle_t f; + int bufferlen; + char *buffer; + char filename[MAX_QPATH]; + + if (Cmd_Argc() != 2) + { + Com_Printf ("usage: condump <filename>\n"); + return; + } + + Q_strncpyz( filename, Cmd_Argv( 1 ), sizeof( filename ) ); + COM_DefaultExtension( filename, sizeof( filename ), ".txt" ); + + if (!COM_CompareExtension(filename, ".txt")) + { + Com_Printf("Con_Dump_f: Only the \".txt\" extension is supported by this command!\n"); + return; + } + + f = FS_FOpenFileWrite( filename ); + if (!f) + { + Com_Printf("ERROR: couldn't open %s.\n", filename); + return; + } + + Com_Printf("Dumped console text to %s.\n", filename ); + + // skip empty lines + for (l = con.current - con.totallines + 1 ; l <= con.current ; l++) + { + line = con.text + (l%con.totallines)*con.linewidth; + for (x=0 ; x<con.linewidth ; x++) + if ((line[x] & 0xff) != ' ') + break; + if (x != con.linewidth) + break; + } + +#ifdef _WIN32 + bufferlen = con.linewidth + 3 * sizeof ( char ); +#else + bufferlen = con.linewidth + 2 * sizeof ( char ); +#endif + + buffer = (char*)Hunk_AllocateTempMemory( bufferlen ); + + // write the remaining lines + buffer[bufferlen-1] = 0; + for ( ; l <= con.current ; l++) + { + line = con.text + (l%con.totallines)*con.linewidth; + for(i=0; i<con.linewidth; i++) + buffer[i] = line[i] & 0xff; + for (x=con.linewidth-1 ; x>=0 ; x--) + { + if (buffer[x] == ' ') + buffer[x] = 0; + else + break; + } +#ifdef _WIN32 + Q_strcat(buffer, bufferlen, "\r\n"); +#else + Q_strcat(buffer, bufferlen, "\n"); +#endif + FS_Write(buffer, strlen(buffer), f); + } + + Hunk_FreeTempMemory( buffer ); + FS_FCloseFile( f ); +} + + +/* +================ +Con_ClearNotify +================ +*/ +void Con_ClearNotify( void ) { + Cmd_TokenizeString( NULL ); + CL_GameConsoleText( ); +} + + + +/* +================ +Con_CheckResize + +If the line width has changed, reformat the buffer. +================ +*/ +void Con_CheckResize (void) +{ + int i, j, width, oldwidth, oldtotallines, numlines, numchars; + short tbuf[CON_TEXTSIZE]; + + width = (SCREEN_WIDTH / SMALLCHAR_WIDTH) - 2; + + if (width == con.linewidth) + return; + + if (width < 1) // video hasn't been initialized yet + { + width = DEFAULT_CONSOLE_WIDTH; + con.linewidth = width; + con.totallines = CON_TEXTSIZE / con.linewidth; + for(i=0; i<CON_TEXTSIZE; i++) + + con.text[i] = (ColorIndex(COLOR_WHITE)<<8) | ' '; + } + else + { + oldwidth = con.linewidth; + con.linewidth = width; + oldtotallines = con.totallines; + con.totallines = CON_TEXTSIZE / con.linewidth; + numlines = oldtotallines; + + if (con.totallines < numlines) + numlines = con.totallines; + + numchars = oldwidth; + + if (con.linewidth < numchars) + numchars = con.linewidth; + + ::memcpy(tbuf, con.text, CON_TEXTSIZE * sizeof(short)); + for(i=0; i<CON_TEXTSIZE; i++) + + con.text[i] = (ColorIndex(COLOR_WHITE)<<8) | ' '; + + + for (i=0 ; i<numlines ; i++) + { + for (j=0 ; j<numchars ; j++) + { + con.text[(con.totallines - 1 - i) * con.linewidth + j] = + tbuf[((con.current - i + oldtotallines) % + oldtotallines) * oldwidth + j]; + } + } + } + + con.current = con.totallines - 1; + con.display = con.current; +} + +/* +================== +Cmd_CompleteTxtName +================== +*/ +void Cmd_CompleteTxtName( char *args UNUSED, int argNum ) { + if( argNum == 2 ) { + Field_CompleteFilename( "", "txt", false, true ); + } +} + +/* +================ +Con_MessageModesInit +================ +*/ +void Con_MessageModesInit(void) { + if( clc.netchan.alternateProtocol == 2 ) + { + // add the client side message modes for 1.1 servers + if( !Cmd_CommadExists( "messagemode" ) ) + Cmd_AddCommand ("messagemode", Con_MessageMode_f); + if( !Cmd_CommadExists( "messagemode2" ) ) + Cmd_AddCommand ("messagemode2", Con_MessageMode2_f); + if( !Cmd_CommadExists( "messagemode3" ) ) + Cmd_AddCommand ("messagemode3", Con_MessageMode3_f); + if( !Cmd_CommadExists( "messagemode4" ) ) + Cmd_AddCommand ("messagemode4", Con_MessageMode4_f); + if( !Cmd_CommadExists( "messagemode5" ) ) + Cmd_AddCommand ("messagemode5", Con_MessageMode5_f); + if( !Cmd_CommadExists( "messagemode6" ) ) + Cmd_AddCommand ("messagemode6", Con_MessageMode6_f); + } else + { + // remove the client side message modes for non-1.1 servers + Cmd_RemoveCommand("messagemode"); + Cmd_RemoveCommand("messagemode2"); + Cmd_RemoveCommand("messagemode3"); + Cmd_RemoveCommand("messagemode4"); + Cmd_RemoveCommand("messagemode5"); + Cmd_RemoveCommand("messagemode6"); + } +} + +/* +================ +Con_Init +================ +*/ +void Con_Init (void) { + int i; + + con_conspeed = Cvar_Get ("scr_conspeed", "3", 0); + con_useShader = Cvar_Get ("scr_useShader", "1", CVAR_ARCHIVE); + con_height = Cvar_Get ("scr_height", "50", CVAR_ARCHIVE); + con_colorRed = Cvar_Get ("scr_colorRed", "0", CVAR_ARCHIVE); + con_colorBlue = Cvar_Get ("scr_colorBlue", "0", CVAR_ARCHIVE); + con_colorGreen = Cvar_Get ("scr_colorGreen", "0", CVAR_ARCHIVE); + con_colorAlpha = Cvar_Get ("scr_colorAlpha", ".8", CVAR_ARCHIVE); + con_versionStr = Cvar_Get ("scr_versionString", Q3_VERSION, CVAR_ARCHIVE); + + Field_Clear( &g_consoleField ); + g_consoleField.widthInChars = g_console_field_width; + for ( i = 0 ; i < COMMAND_HISTORY ; i++ ) { + Field_Clear( &historyEditLines[i] ); + historyEditLines[i].widthInChars = g_console_field_width; + } + CL_LoadConsoleHistory( ); + if( clc.netchan.alternateProtocol == 2 ) + { + Cmd_AddCommand ("messagemode", Con_MessageMode_f); + Cmd_AddCommand ("messagemode2", Con_MessageMode2_f); + Cmd_AddCommand ("messagemode3", Con_MessageMode3_f); + Cmd_AddCommand ("messagemode4", Con_MessageMode4_f); + Cmd_AddCommand ("messagemode5", Con_MessageMode5_f); + Cmd_AddCommand ("messagemode6", Con_MessageMode6_f); + } + + Cmd_AddCommand ("toggleconsole", Con_ToggleConsole_f); + Cmd_AddCommand ("togglemenu", Con_ToggleMenu_f); + Cmd_AddCommand ("clear", Con_Clear_f); + Cmd_AddCommand ("condump", Con_Dump_f); + Cmd_SetCommandCompletionFunc( "condump", Cmd_CompleteTxtName ); +} + +/* +================ +Con_Shutdown +================ +*/ +void Con_Shutdown(void) +{ + Cmd_RemoveCommand("toggleconsole"); + Cmd_RemoveCommand("togglemenu"); + Cmd_RemoveCommand("clear"); + Cmd_RemoveCommand("condump"); +} + +/* +=============== +Con_Linefeed +=============== +*/ +void Con_Linefeed(void) +{ + int i; + + con.x = 0; + + if (con.display == con.current) + con.display++; + + con.current++; + + for(i=0; i<con.linewidth; i++) + con.text[(con.current%con.totallines)*con.linewidth+i] = (ColorIndex(COLOR_WHITE)<<8) | ' '; +} + +/* +================ +CL_ConsolePrint + +Handles cursor positioning, line wrapping, etc +All console printing must go through this in order to be logged to disk +If no console is visible, the text will appear at the top of the game window +================ +*/ +void CL_ConsolePrint( const char *txt ) +{ + int y, l; + unsigned char c; + unsigned short color; + bool skipnotify = false; // NERVE - SMF + + // TTimo - prefix for text that shows up in console but not in notify + // backported from RTCW + if ( !Q_strncmp( txt, "[skipnotify]", 12 ) ) { + skipnotify = true; + txt += 12; + } + + // for some demos we don't want to ever show anything on the console + if ( cl_noprint && cl_noprint->integer ) + return; + + if (!con.initialized) { + con.color[0] = + con.color[1] = + con.color[2] = + con.color[3] = 1.0f; + con.linewidth = -1; + Con_CheckResize (); + con.initialized = true; + } + + if( !skipnotify && !(Key_GetCatcher( ) & KEYCATCH_CONSOLE) ) + { + Cmd_SaveCmdContext( ); + + // feed the text to cgame + Cmd_TokenizeString( txt ); + CL_GameConsoleText( ); + + Cmd_RestoreCmdContext( ); + } + + color = ColorIndex(COLOR_WHITE); + + while ( (c = *((unsigned char *)txt)) != 0 ) + { + if ( Q_IsColorString( txt ) ) + { + color = ColorIndex( *(txt+1) ); + txt += 2; + continue; + } + + // count word length + for (l=0 ; l< con.linewidth ; l++) + { + if ( txt[l] <= ' ') + break; + } + + // word wrap + if (l != con.linewidth && (con.x + l >= con.linewidth) ) + Con_Linefeed(); + + txt++; + + switch (c) + { + case INDENT_MARKER: + break; + case '\n': + Con_Linefeed(); + break; + case '\r': + con.x = 0; + break; + default: // display character and advance + y = con.current % con.totallines; + con.text[y*con.linewidth+con.x] = (color << 8) | c; + con.x++; + if(con.x >= con.linewidth) + Con_Linefeed(); + break; + } + } +} + + +/* +============================================================================== + +DRAWING + +============================================================================== +*/ + + +/* +================ +Con_DrawInput + +Draw the editline after a ] prompt +================ +*/ +static void Con_DrawInput (void) +{ + int y; + + if ( clc.state != CA_DISCONNECTED && !(Key_GetCatcher( ) & KEYCATCH_CONSOLE ) ) { + return; + } + + y = con.vislines - ( SMALLCHAR_HEIGHT * 2 ); + + re.SetColor( con.color ); + + SCR_DrawSmallChar( con.xadjust + 1 * SMALLCHAR_WIDTH, y, ']' ); + + Field_Draw( &g_consoleField, + con.xadjust + 2 * SMALLCHAR_WIDTH, + y, + SCREEN_WIDTH - 3 * SMALLCHAR_WIDTH, + true, true ); +} + +/* +================ +Con_DrawSolidConsole + +Draws the console with the solid background +================ +*/ +static void Con_DrawSolidConsole( float frac ) +{ + int i, x, y; + int rows; + short *text; + int row; + int lines; + int currentColor; + vec4_t color; + + lines = cls.glconfig.vidHeight * frac; + if (lines <= 0) + return; + + if (lines > cls.glconfig.vidHeight ) + lines = cls.glconfig.vidHeight; + + // on wide screens, we will center the text + con.xadjust = 0; + SCR_AdjustFrom640( &con.xadjust, NULL, NULL, NULL ); + + // draw the background + y = frac * SCREEN_HEIGHT; + if ( y < 1 ) + { + y = 0; + } + else if (con_useShader->integer) + { + SCR_DrawPic(0, 0, SCREEN_WIDTH, y, cls.consoleShader); + } + else + { + color[0] = con_colorRed->value; + color[1] = con_colorGreen->value; + color[2] = con_colorBlue->value; + color[3] = con_colorAlpha->value; + SCR_FillRect(0, 0, SCREEN_WIDTH, y, color); + } + + color[0] = 1; + color[1] = 0; + color[2] = 0; + color[3] = 1; + SCR_FillRect(0, y, SCREEN_WIDTH, 2, color); + + + // draw the version number + + re.SetColor( g_color_table[ColorIndex(COLOR_RED)] ); + + i = strlen( Q3_VERSION ); + + for (x=0 ; x<i ; x++) { + SCR_DrawSmallChar( cls.glconfig.vidWidth - ( i - x + 1 ) * SMALLCHAR_WIDTH, + lines - SMALLCHAR_HEIGHT, Q3_VERSION[x] ); + } + + // draw the text + con.vislines = lines; + rows = (lines-SMALLCHAR_HEIGHT)/SMALLCHAR_HEIGHT; // rows of text to draw + + y = lines - (SMALLCHAR_HEIGHT*3); + + // draw from the bottom up + if (con.display != con.current) + { + // draw arrows to show the buffer is backscrolled + re.SetColor( g_color_table[ColorIndex(COLOR_RED)] ); + for (x=0 ; x<con.linewidth ; x+=4) + SCR_DrawSmallChar( con.xadjust + (x+1)*SMALLCHAR_WIDTH, y, '^' ); + y -= SMALLCHAR_HEIGHT; + rows--; + } + + row = con.display; + + if ( con.x == 0 ) { + row--; + } + + currentColor = 7; + re.SetColor( g_color_table[currentColor] ); + + for (i=0 ; i<rows ; i++, y -= SMALLCHAR_HEIGHT, row--) + { + if (row < 0) + break; + if (con.current - row >= con.totallines) { + // past scrollback wrap point + continue; + } + + text = con.text + (row % con.totallines)*con.linewidth; + + for (x=0 ; x<con.linewidth ; x++) { + if ( ( text[x] & 0xff ) == ' ' ) { + continue; + } + + if ( ColorIndexForNumber( text[x]>>8 ) != currentColor ) { + currentColor = ColorIndexForNumber( text[x]>>8 ); + re.SetColor( g_color_table[currentColor] ); + } + SCR_DrawSmallChar( con.xadjust + (x+1)*SMALLCHAR_WIDTH, y, text[x] & 0xff ); + } + } + + // draw the input prompt, user text, and cursor if desired + Con_DrawInput (); + + re.SetColor( NULL ); +} + + + +/* +================== +Con_DrawConsole +================== +*/ +void Con_DrawConsole( void ) { + // check for console width changes from a vid mode change + Con_CheckResize (); + + // if disconnected, render console full screen + if ( clc.state == CA_DISCONNECTED ) { + if ( !( Key_GetCatcher( ) & (KEYCATCH_UI | KEYCATCH_CGAME)) ) { + Con_DrawSolidConsole( 1.0 ); + return; + } + } + + // draw the chat line + if( clc.netchan.alternateProtocol == 2 && + ( Key_GetCatcher( ) & KEYCATCH_MESSAGE ) ) + { + int skip; + + if( chatField.buffer[0] == '/' || + chatField.buffer[0] == '\\' ) + { + SCR_DrawBigString( 8, 232, "Command:", 1.0f, qfalse ); + skip = 10; + } + else if( chat_team ) + { + SCR_DrawBigString( 8, 232, "Team Say:", 1.0f, qfalse ); + skip = 11; + } + else if( chat_admins ) + { + SCR_DrawBigString( 8, 232, "Admin Say:", 1.0f, qfalse ); + skip = 11; + } + else if( chat_clans ) + { + SCR_DrawBigString( 8, 232, "Clan Say:", 1.0f, qfalse ); + skip = 11; + } + else + { + SCR_DrawBigString( 8, 232, "Say:", 1.0f, qfalse ); + skip = 5; + } + + Field_BigDraw( &chatField, skip * BIGCHAR_WIDTH, 232, + SCREEN_WIDTH - ( skip + 1 ) * BIGCHAR_WIDTH, qtrue, qtrue ); + } + + if ( con.displayFrac ) { + Con_DrawSolidConsole( con.displayFrac ); + } + + if( Key_GetCatcher( ) & ( KEYCATCH_UI | KEYCATCH_CGAME ) ) + return; +} +//================================================================ + +/* +================== +Con_RunConsole + +Scroll it up or down +================== +*/ +void Con_RunConsole (void) { + // decide on the destination height of the console + if ( Key_GetCatcher( ) & KEYCATCH_CONSOLE ) + con.finalFrac = MAX(0.10, 0.01 * con_height->integer); + else + con.finalFrac = 0; // none visible + + // scroll towards the destination height + if (con.finalFrac < con.displayFrac) + { + con.displayFrac -= con_conspeed->value*cls.realFrametime*0.001; + if (con.finalFrac > con.displayFrac) + con.displayFrac = con.finalFrac; + + } + else if (con.finalFrac > con.displayFrac) + { + con.displayFrac += con_conspeed->value*cls.realFrametime*0.001; + if (con.finalFrac < con.displayFrac) + con.displayFrac = con.finalFrac; + } + +} + + +void Con_PageUp( void ) { + con.display -= 2; + if ( con.current - con.display >= con.totallines ) { + con.display = con.current - con.totallines + 1; + } +} + +void Con_PageDown( void ) { + con.display += 2; + if (con.display > con.current) { + con.display = con.current; + } +} + +void Con_Top( void ) { + con.display = con.totallines; + if ( con.current - con.display >= con.totallines ) { + con.display = con.current - con.totallines + 1; + } +} + +void Con_Bottom( void ) { + con.display = con.current; +} + + +void Con_Close( void ) { + if ( !com_cl_running->integer ) { + return; + } + Field_Clear( &g_consoleField ); + Key_SetCatcher( Key_GetCatcher( ) & ~KEYCATCH_CONSOLE ); + con.finalFrac = 0; // none visible + con.displayFrac = 0; +} diff --git a/src/client/cl_curl.cpp b/src/client/cl_curl.cpp new file mode 100644 index 0000000..0c81985 --- /dev/null +++ b/src/client/cl_curl.cpp @@ -0,0 +1,364 @@ +/* +=========================================================================== +Copyright (C) 2006 Tony J. White (tjw@tjw.org) +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#include "client.h" + +#ifdef USE_CURL_DLOPEN +#include "sys/sys_loadlib.h" + +cvar_t *cl_cURLLib; + +char* (*qcurl_version)(void); + +CURL* (*qcurl_easy_init)(void); +CURLcode (*qcurl_easy_setopt)(CURL *curl, CURLoption option, ...); +CURLcode (*qcurl_easy_perform)(CURL *curl); +void (*qcurl_easy_cleanup)(CURL *curl); +CURLcode (*qcurl_easy_getinfo)(CURL *curl, CURLINFO info, ...); +CURL* (*qcurl_easy_duphandle)(CURL *curl); +void (*qcurl_easy_reset)(CURL *curl); +const char *(*qcurl_easy_strerror)(CURLcode); + +CURLM* (*qcurl_multi_init)(void); +CURLMcode (*qcurl_multi_add_handle)(CURLM*, CURL*curl_handle); +CURLMcode (*qcurl_multi_remove_handle)(CURLM*, CURL*); +CURLMcode (*qcurl_multi_fdset)(CURLM*, fd_set* read_set, fd_set* write_set, fd_set* exc_set, int*); +CURLMcode (*qcurl_multi_perform)(CURLM*, int*); +CURLMcode (*qcurl_multi_cleanup)(CURLM*); +CURLMsg *(*qcurl_multi_info_read)(CURLM*, int*); +const char *(*qcurl_multi_strerror)(CURLMcode); + +struct curl_slist* (*qcurl_slist_append)(struct curl_slist*, const char*); +void (*qcurl_slist_free_all)(struct curl_slist*); + +CURLcode (*qcurl_global_init)(long); +void (*qcurl_global_cleanup)(void); + + +static void *cURLLib = NULL; + +/* +================= +GPA +================= +*/ +static void *GPA(const char *str) +{ + void* rv = Sys_LoadFunction(cURLLib, str); + if(!rv) + { + Com_Printf("Can't load symbol %s\n", str); + clc.cURLEnabled = false; + return NULL; + } + else + { + Com_DPrintf("Loaded symbol %s (0x%p)\n", str, rv); + return rv; + } +} +#endif /* USE_CURL_DLOPEN */ + +/* +================= +CL_cURL_Init +================= +*/ +bool CL_cURL_Init() +{ +#ifdef USE_CURL_DLOPEN + cl_cURLLib = Cvar_Get("cl_cURLLib", DEFAULT_CURL_LIB, CVAR_ARCHIVE | CVAR_PROTECTED); + + if(cURLLib) + return true; + + Com_Printf("Loading \"%s\"...", cl_cURLLib->string); + if( !(cURLLib = Sys_LoadDll(cl_cURLLib->string, true)) ) + { +#ifdef ALTERNATE_CURL_LIB + // On some linux distributions there is no libcurl.so.3, but only libcurl.so.4. That one works too. + if( !(cURLLib = Sys_LoadDll(ALTERNATE_CURL_LIB, true)) ) +#endif + return false; + } + + clc.cURLEnabled = true; + + qcurl_version = (decltype(qcurl_version)) GPA("curl_version"); + + qcurl_easy_init = (decltype(qcurl_easy_init)) GPA("curl_easy_init"); + qcurl_easy_setopt = (decltype(qcurl_easy_setopt)) GPA("curl_easy_setopt"); + qcurl_easy_perform = (decltype(qcurl_easy_perform)) GPA("curl_easy_perform"); + qcurl_easy_cleanup = (decltype(qcurl_easy_cleanup)) GPA("curl_easy_cleanup"); + qcurl_easy_getinfo = (decltype(qcurl_easy_getinfo)) GPA("curl_easy_getinfo"); + qcurl_easy_duphandle = (decltype(qcurl_easy_duphandle)) GPA("curl_easy_duphandle"); + qcurl_easy_reset = (decltype(qcurl_easy_reset)) GPA("curl_easy_reset"); + qcurl_easy_strerror = (decltype(qcurl_easy_strerror)) GPA("curl_easy_strerror"); + + qcurl_multi_init = (decltype(qcurl_multi_init)) GPA("curl_multi_init"); + qcurl_multi_add_handle = (decltype(qcurl_multi_add_handle)) GPA("curl_multi_add_handle"); + qcurl_multi_remove_handle = (decltype(qcurl_multi_remove_handle)) GPA("curl_multi_remove_handle"); + qcurl_multi_fdset = (decltype(qcurl_multi_fdset)) GPA("curl_multi_fdset"); + qcurl_multi_perform = (decltype(qcurl_multi_perform)) GPA("curl_multi_perform"); + qcurl_multi_cleanup = (decltype(qcurl_multi_cleanup)) GPA("curl_multi_cleanup"); + qcurl_multi_info_read = (decltype(qcurl_multi_info_read)) GPA("curl_multi_info_read"); + qcurl_multi_strerror = (decltype(qcurl_multi_strerror)) GPA("curl_multi_strerror"); + qcurl_slist_append = (decltype(qcurl_slist_append)) GPA("curl_slist_append"); + qcurl_slist_free_all = (decltype(qcurl_slist_free_all)) GPA("curl_slist_free_all"); + qcurl_global_init = (decltype(qcurl_global_init)) GPA("curl_global_init"); + qcurl_global_cleanup = (decltype(qcurl_global_cleanup)) GPA("curl_global_cleanup"); + + if(!clc.cURLEnabled) + { + CL_cURL_Shutdown(); + Com_Printf("FAIL One or more symbols not found\n"); + return false; + } + Com_Printf("OK\n"); + + return true; +#else + clc.cURLEnabled = true; + return true; +#endif /* USE_CURL_DLOPEN */ +} + +/* +================= +CL_cURL_Shutdown +================= +*/ +void CL_cURL_Shutdown( void ) +{ + CL_cURL_Cleanup(); +#ifdef USE_CURL_DLOPEN + if(cURLLib) + { + Sys_UnloadLibrary(cURLLib); + cURLLib = NULL; + } + qcurl_easy_init = NULL; + qcurl_easy_setopt = NULL; + qcurl_easy_perform = NULL; + qcurl_easy_cleanup = NULL; + qcurl_easy_getinfo = NULL; + qcurl_easy_duphandle = NULL; + qcurl_easy_reset = NULL; + + qcurl_multi_init = NULL; + qcurl_multi_add_handle = NULL; + qcurl_multi_remove_handle = NULL; + qcurl_multi_fdset = NULL; + qcurl_multi_perform = NULL; + qcurl_multi_cleanup = NULL; + qcurl_multi_info_read = NULL; + qcurl_multi_strerror = NULL; +#endif /* USE_CURL_DLOPEN */ +} + +void CL_cURL_Cleanup(void) +{ + if(clc.downloadCURLM) { + CURLMcode result; + + if(clc.downloadCURL) { + result = qcurl_multi_remove_handle(clc.downloadCURLM, + clc.downloadCURL); + if(result != CURLM_OK) { + Com_DPrintf("qcurl_multi_remove_handle failed: %s\n", qcurl_multi_strerror(result)); + } + qcurl_easy_cleanup(clc.downloadCURL); + } + result = qcurl_multi_cleanup(clc.downloadCURLM); + if(result != CURLM_OK) { + Com_DPrintf("CL_cURL_Cleanup: qcurl_multi_cleanup failed: %s\n", qcurl_multi_strerror(result)); + } + clc.downloadCURLM = NULL; + clc.downloadCURL = NULL; + } + else if(clc.downloadCURL) { + qcurl_easy_cleanup(clc.downloadCURL); + clc.downloadCURL = NULL; + } +} + +static int CL_cURL_CallbackProgress( void *dummy, double dltotal, double dlnow, + double ultotal, double ulnow ) +{ + clc.downloadSize = (int)dltotal; + Cvar_SetValue( "cl_downloadSize", clc.downloadSize ); + clc.downloadCount = (int)dlnow; + Cvar_SetValue( "cl_downloadCount", clc.downloadCount ); + return 0; +} + +static size_t CL_cURL_CallbackWrite(void *buffer, size_t size, size_t nmemb, + void *stream) +{ + FS_Write( buffer, size*nmemb, ((fileHandle_t*)stream)[0] ); + return size*nmemb; +} + +CURLcode qcurl_easy_setopt_warn(CURL *curl, CURLoption option, ...) +{ + CURLcode result; + + va_list argp; + va_start(argp, option); + + if(option < CURLOPTTYPE_OBJECTPOINT) { + long longValue = va_arg(argp, long); + result = qcurl_easy_setopt(curl, option, longValue); + } else if(option < CURLOPTTYPE_OFF_T) { + void *pointerValue = va_arg(argp, void *); + result = qcurl_easy_setopt(curl, option, pointerValue); + } else { + curl_off_t offsetValue = va_arg(argp, curl_off_t); + result = qcurl_easy_setopt(curl, option, offsetValue); + } + + if(result != CURLE_OK) { + Com_DPrintf("qcurl_easy_setopt failed: %s\n", qcurl_easy_strerror(result)); + } + va_end(argp); + + return result; +} + +void CL_cURL_BeginDownload( const char *localName, const char *remoteURL ) +{ + CURLMcode result; + + clc.cURLUsed = true; + Com_Printf("URL: %s\n", remoteURL); + Com_DPrintf("***** CL_cURL_BeginDownload *****\n" + "Localname: %s\n" + "RemoteURL: %s\n" + "****************************\n", localName, remoteURL); + CL_cURL_Cleanup(); + Q_strncpyz(clc.downloadURL, remoteURL, sizeof(clc.downloadURL)); + Q_strncpyz(clc.downloadName, localName, sizeof(clc.downloadName)); + Com_sprintf(clc.downloadTempName, sizeof(clc.downloadTempName), + "%s.tmp", localName); + + // Set so UI gets access to it + Cvar_Set("cl_downloadName", localName); + Cvar_Set("cl_downloadSize", "0"); + Cvar_Set("cl_downloadCount", "0"); + Cvar_SetValue("cl_downloadTime", cls.realtime); + + clc.downloadBlock = 0; // Starting new file + clc.downloadCount = 0; + + clc.downloadCURL = qcurl_easy_init(); + if(!clc.downloadCURL) { + Com_Error(ERR_DROP, "CL_cURL_BeginDownload: qcurl_easy_init() " + "failed"); + return; + } + clc.download = FS_SV_FOpenFileWrite(clc.downloadTempName); + if(!clc.download) { + Com_Error(ERR_DROP, "CL_cURL_BeginDownload: failed to open " + "%s for writing", clc.downloadTempName); + return; + } + + if(com_developer->integer) + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_VERBOSE, 1); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_URL, clc.downloadURL); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_TRANSFERTEXT, 0); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_REFERER, va("Tremulous://%s", NET_AdrToString(clc.serverAddress))); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_USERAGENT, va("%s %s", Q3_VERSION, qcurl_version())); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_WRITEFUNCTION, CL_cURL_CallbackWrite); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_WRITEDATA, &clc.download); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_NOPROGRESS, 0); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_PROGRESSFUNCTION, CL_cURL_CallbackProgress); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_PROGRESSDATA, NULL); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_FAILONERROR, 1); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_FOLLOWLOCATION, 1); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_MAXREDIRS, 5); + qcurl_easy_setopt_warn(clc.downloadCURL, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS | CURLPROTO_FTP | CURLPROTO_FTPS ); + clc.downloadCURLM = qcurl_multi_init(); + if(!clc.downloadCURLM) + { + qcurl_easy_cleanup(clc.downloadCURL); + clc.downloadCURL = NULL; + Com_Error(ERR_DROP, "CL_cURL_BeginDownload: qcurl_multi_init() " + "failed"); + return; + } + + result = qcurl_multi_add_handle(clc.downloadCURLM, clc.downloadCURL); + if(result != CURLM_OK) + { + qcurl_easy_cleanup(clc.downloadCURL); + clc.downloadCURL = NULL; + Com_Error(ERR_DROP,"CL_cURL_BeginDownload: qcurl_multi_add_handle() failed: %s", qcurl_multi_strerror(result)); + return; + } + + if(!(clc.sv_allowDownload & DLF_NO_DISCONNECT) && !clc.cURLDisconnected) + { + CL_AddReliableCommand("disconnect", true); + CL_WritePacket(); + CL_WritePacket(); + CL_WritePacket(); + clc.cURLDisconnected = true; + } +} + +void CL_cURL_PerformDownload(void) +{ + CURLMcode res; + CURLMsg *msg; + int c; + int i = 0; + + res = qcurl_multi_perform(clc.downloadCURLM, &c); + while(res == CURLM_CALL_MULTI_PERFORM && i < 100) { + res = qcurl_multi_perform(clc.downloadCURLM, &c); + i++; + } + if(res == CURLM_CALL_MULTI_PERFORM) + return; + msg = qcurl_multi_info_read(clc.downloadCURLM, &c); + if(msg == NULL) { + return; + } + FS_FCloseFile(clc.download); + if(msg->msg == CURLMSG_DONE && msg->data.result == CURLE_OK) { + FS_SV_Rename(clc.downloadTempName, clc.downloadName, false); + clc.downloadRestart = true; + } + else { + long code; + + qcurl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &code); + Com_Error(ERR_DROP, "Download Error: %s Code: %ld URL: %s", + qcurl_easy_strerror(msg->data.result), + code, clc.downloadURL); + } + + CL_NextDownload(); +} diff --git a/src/client/cl_curl.h b/src/client/cl_curl.h new file mode 100644 index 0000000..2e3b4e3 --- /dev/null +++ b/src/client/cl_curl.h @@ -0,0 +1,101 @@ +/* +=========================================================================== +Copyright (C) 2006 Tony J. White (tjw@tjw.org) +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#ifndef __QCURL_H__ +#define __QCURL_H__ + +#ifdef USE_LOCAL_HEADERS +#include "curl/curl.h" +#else +#include <curl/curl.h> +#endif + +#include "qcommon/q_shared.h" +#include "qcommon/qcommon.h" + +#ifdef USE_CURL_DLOPEN +#ifdef _WIN32 + #define DEFAULT_CURL_LIB "libcurl-4.dll" + #define ALTERNATE_CURL_LIB "libcurl-3.dll" +#elif defined(__APPLE__) + #define DEFAULT_CURL_LIB "libcurl.dylib" +#else + #define DEFAULT_CURL_LIB "libcurl.so.4" + #define ALTERNATE_CURL_LIB "libcurl.so.3" +#endif + +extern cvar_t *cl_cURLLib; + +extern char* (*qcurl_version)(void); +extern CURL* (*qcurl_easy_init)(void); +extern CURLcode (*qcurl_easy_setopt)(CURL *curl, CURLoption option, ...); +extern CURLcode (*qcurl_easy_perform)(CURL *curl); +extern void (*qcurl_easy_cleanup)(CURL *curl); +extern CURLcode (*qcurl_easy_getinfo)(CURL *curl, CURLINFO info, ...); +extern void (*qcurl_easy_reset)(CURL *curl); +extern const char* (*qcurl_easy_strerror)(CURLcode); +extern CURLM* (*qcurl_multi_init)(void); +extern CURLMcode (*qcurl_multi_add_handle)(CURLM *multi_handle, CURL *curl_handle); +extern CURLMcode (*qcurl_multi_remove_handle)(CURLM *multi_handle, CURL *curl_handle); +extern CURLMcode (*qcurl_multi_fdset)(CURLM *multi_handle, fd_set *read_fd_set, fd_set *write_fd_set, fd_set *exc_fd_set, int *max_fd); +extern CURLMcode (*qcurl_multi_perform)(CURLM *multi_handle, int *running_handles); +extern CURLMcode (*qcurl_multi_cleanup)(CURLM *multi_handle); +extern CURLMsg* (*qcurl_multi_info_read)(CURLM *multi_handle, int *msgs_in_queue); +extern const char* (*qcurl_multi_strerror)(CURLMcode); +extern struct curl_slist* (*qcurl_slist_append)(struct curl_slist *, const char *); +extern void (*qcurl_slist_free_all)(struct curl_slist *); +extern CURLcode (*qcurl_global_init)(long flags); +extern void (*qcurl_global_cleanup)(void); + +#else +#define qcurl_version curl_version +#define qcurl_easy_init curl_easy_init +#define qcurl_easy_setopt curl_easy_setopt +#define qcurl_easy_perform curl_easy_perform +#define qcurl_easy_cleanup curl_easy_cleanup +#define qcurl_easy_getinfo curl_easy_getinfo +#define qcurl_easy_duphandle curl_easy_duphandle +#define qcurl_easy_reset curl_easy_reset +#define qcurl_easy_strerror curl_easy_strerror +#define qcurl_multi_init curl_multi_init +#define qcurl_multi_add_handle curl_multi_add_handle +#define qcurl_multi_remove_handle curl_multi_remove_handle +#define qcurl_multi_fdset curl_multi_fdset +#define qcurl_multi_perform curl_multi_perform +#define qcurl_multi_cleanup curl_multi_cleanup +#define qcurl_multi_info_read curl_multi_info_read +#define qcurl_multi_strerror curl_multi_strerror +#define qcurl_slist_append curl_slist_append +#define qcurl_slist_free_all curl_slist_free_all +#define qcurl_global_init curl_global_init +#define qcurl_global_cleanup curl_global_cleanup +#endif + +bool CL_cURL_Init( void ); +void CL_cURL_Shutdown( void ); +void CL_cURL_BeginDownload( const char *localName, const char *remoteURL ); +void CL_cURL_PerformDownload( void ); +void CL_cURL_Cleanup( void ); + +#endif // __QCURL_H__ diff --git a/src/client/cl_input.cpp b/src/client/cl_input.cpp new file mode 100644 index 0000000..e134646 --- /dev/null +++ b/src/client/cl_input.cpp @@ -0,0 +1,1194 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +// cl.input.c -- builds an intended movement command to send to the server + +#include "client.h" + +unsigned frame_msec; +int old_com_frameTime; + +/* +=============================================================================== + +KEY BUTTONS + +Continuous button event tracking is complicated by the fact that two different +input sources (say, mouse button 1 and the control key) can both press the +same button, but the button should only be released when both of the +pressing key have been released. + +When a key event issues a button command (+forward, +attack, etc), it appends +its key number as argv(1) so it can be matched up with the release. + +argv(2) will be set to the time the event happened, which allows exact +control even at low framerates when the down and up events may both get qued +at the same time. + +=============================================================================== +*/ + +kbutton_t in_left, in_right, in_forward, in_back; +kbutton_t in_lookup, in_lookdown, in_moveleft, in_moveright; +kbutton_t in_strafe, in_speed; +kbutton_t in_up, in_down; + +#ifdef USE_VOIP +kbutton_t in_voiprecord; +#endif + +kbutton_t in_buttons[16]; + +bool in_mlooking; + +static void IN_CenterView(void) +{ + cl.viewangles[PITCH] = -SHORT2ANGLE( (clc.netchan.alternateProtocol == 2 + ? cl.snap.alternatePs.delta_angles + : cl.snap.ps.delta_angles)[PITCH] ); +} + +static void IN_MLookDown(void) +{ + in_mlooking = true; +} + +static void IN_MLookUp(void) +{ + in_mlooking = false; + if (!cl_freelook->integer) + { + IN_CenterView(); + } +} + +static void IN_KeyDown(kbutton_t *b) +{ + int k; + + const char *c = Cmd_Argv(1); + if (c[0]) + { + k = atoi(c); + } + else + { + k = -1; // typed manually at the console for continuous down + } + + if (k == b->down[0] || k == b->down[1]) + { + return; // repeating key + } + + if (!b->down[0]) + { + b->down[0] = k; + } + else if (!b->down[1]) + { + b->down[1] = k; + } + else + { + Com_Printf("Three keys down for a button!\n"); + return; + } + + if (b->active) + { + return; // still down + } + + // save timestamp for partial frame summing + c = Cmd_Argv(2); + b->downtime = atoi(c); + + b->active = true; + b->wasPressed = true; +} + +static void IN_KeyUp(kbutton_t *b) +{ + int k; + unsigned uptime; + + const char *c = Cmd_Argv(1); + if (c[0]) + { + k = atoi(c); + } + else + { + // typed manually at the console, assume for unsticking, so clear all + b->down[0] = b->down[1] = 0; + b->active = false; + return; + } + + if (b->down[0] == k) + { + b->down[0] = 0; + } + else if (b->down[1] == k) + { + b->down[1] = 0; + } + else + { + return; // key up without coresponding down (menu pass through) + } + if (b->down[0] || b->down[1]) + { + return; // some other key is still holding it down + } + + b->active = false; + + // save timestamp for partial frame summing + c = Cmd_Argv(2); + uptime = atoi(c); + if (uptime) + { + b->msec += uptime - b->downtime; + } + else + { + b->msec += frame_msec / 2; + } + + b->active = false; +} + +/* +=============== +CL_KeyState + +Returns the fraction of the frame that the key was down +=============== +*/ +static float CL_KeyState(kbutton_t *key) +{ + float val; + int msec; + + msec = key->msec; + key->msec = 0; + + if (key->active) + { + // still down + if (!key->downtime) + { + msec = com_frameTime; + } + else + { + msec += com_frameTime - key->downtime; + } + key->downtime = com_frameTime; + } + +#if 0 + if (msec) { + Com_Printf ("%i ", msec); + } +#endif + + val = (float)msec / frame_msec; + if (val < 0) + { + val = 0; + } + if (val > 1) + { + val = 1; + } + + return val; +} + +static void IN_UpDown(void) { IN_KeyDown(&in_up); } +static void IN_UpUp(void) { IN_KeyUp(&in_up); } +static void IN_DownDown(void) { IN_KeyDown(&in_down); } +static void IN_DownUp(void) { IN_KeyUp(&in_down); } +static void IN_LeftDown(void) { IN_KeyDown(&in_left); } +static void IN_LeftUp(void) { IN_KeyUp(&in_left); } +static void IN_RightDown(void) { IN_KeyDown(&in_right); } +static void IN_RightUp(void) { IN_KeyUp(&in_right); } +static void IN_ForwardDown(void) { IN_KeyDown(&in_forward); } +static void IN_ForwardUp(void) { IN_KeyUp(&in_forward); } +static void IN_BackDown(void) { IN_KeyDown(&in_back); } +static void IN_BackUp(void) { IN_KeyUp(&in_back); } +static void IN_LookupDown(void) { IN_KeyDown(&in_lookup); } +static void IN_LookupUp(void) { IN_KeyUp(&in_lookup); } +static void IN_LookdownDown(void) { IN_KeyDown(&in_lookdown); } +static void IN_LookdownUp(void) { IN_KeyUp(&in_lookdown); } +static void IN_MoveleftDown(void) { IN_KeyDown(&in_moveleft); } +static void IN_MoveleftUp(void) { IN_KeyUp(&in_moveleft); } +static void IN_MoverightDown(void) { IN_KeyDown(&in_moveright); } +static void IN_MoverightUp(void) { IN_KeyUp(&in_moveright); } +static void IN_SpeedDown(void) { IN_KeyDown(&in_speed); } +static void IN_SpeedUp(void) { IN_KeyUp(&in_speed); } +static void IN_StrafeDown(void) { IN_KeyDown(&in_strafe); } +static void IN_StrafeUp(void) { IN_KeyUp(&in_strafe); } + +#ifdef USE_VOIP +static void IN_VoipRecordDown(void) +{ + IN_KeyDown(&in_voiprecord); + Cvar_Set("cl_voipSend", "1"); +} + +static void IN_VoipRecordUp(void) +{ + IN_KeyUp(&in_voiprecord); + Cvar_Set("cl_voipSend", "0"); +} +#endif + +static void IN_Button0Down(void) { IN_KeyDown(&in_buttons[0]); } +static void IN_Button0Up(void) { IN_KeyUp(&in_buttons[0]); } +static void IN_Button1Down(void) { IN_KeyDown(&in_buttons[1]); } +static void IN_Button1Up(void) { IN_KeyUp(&in_buttons[1]); } +static void IN_Button2Down(void) { IN_KeyDown(&in_buttons[2]); } +static void IN_Button2Up(void) { IN_KeyUp(&in_buttons[2]); } +static void IN_Button3Down(void) { IN_KeyDown(&in_buttons[3]); } +static void IN_Button3Up(void) { IN_KeyUp(&in_buttons[3]); } +static void IN_Button4Down(void) { IN_KeyDown(&in_buttons[4]); } +static void IN_Button4Up(void) { IN_KeyUp(&in_buttons[4]); } +static void IN_Button5Down(void) { IN_KeyDown(&in_buttons[5]); } +static void IN_Button5Up(void) { IN_KeyUp(&in_buttons[5]); } +static void IN_Button6Down(void) { IN_KeyDown(&in_buttons[6]); } +static void IN_Button6Up(void) { IN_KeyUp(&in_buttons[6]); } +static void IN_Button7Down(void) { IN_KeyDown(&in_buttons[7]); } +static void IN_Button7Up(void) { IN_KeyUp(&in_buttons[7]); } +static void IN_Button8Down(void) { IN_KeyDown(&in_buttons[8]); } +static void IN_Button8Up(void) { IN_KeyUp(&in_buttons[8]); } +static void IN_Button9Down(void) { IN_KeyDown(&in_buttons[9]); } +static void IN_Button9Up(void) { IN_KeyUp(&in_buttons[9]); } +static void IN_Button10Down(void) { IN_KeyDown(&in_buttons[10]); } +static void IN_Button10Up(void) { IN_KeyUp(&in_buttons[10]); } +static void IN_Button11Down(void) { IN_KeyDown(&in_buttons[11]); } +static void IN_Button11Up(void) { IN_KeyUp(&in_buttons[11]); } +static void IN_Button12Down(void) { IN_KeyDown(&in_buttons[12]); } +static void IN_Button12Up(void) { IN_KeyUp(&in_buttons[12]); } +static void IN_Button13Down(void) { IN_KeyDown(&in_buttons[13]); } +static void IN_Button13Up(void) { IN_KeyUp(&in_buttons[13]); } +static void IN_Button14Down(void) { IN_KeyDown(&in_buttons[14]); } +static void IN_Button14Up(void) { IN_KeyUp(&in_buttons[14]); } +static void IN_Button15Down(void) { IN_KeyDown(&in_buttons[15]); } +static void IN_Button15Up(void) { IN_KeyUp(&in_buttons[15]); } + +//========================================================================== + +cvar_t *cl_yawspeed; +cvar_t *cl_pitchspeed; + +cvar_t *cl_run; + +cvar_t *cl_anglespeedkey; + +/* +================ +CL_AdjustAngles + +Moves the local angle positions +================ +*/ +static void CL_AdjustAngles(void) +{ + float speed; + + if (in_speed.active) + { + speed = 0.001 * cls.frametime * cl_anglespeedkey->value; + } + else + { + speed = 0.001 * cls.frametime; + } + + if (!in_strafe.active) + { + cl.viewangles[YAW] -= speed * cl_yawspeed->value * CL_KeyState(&in_right); + cl.viewangles[YAW] += speed * cl_yawspeed->value * CL_KeyState(&in_left); + } + + cl.viewangles[PITCH] -= speed * cl_pitchspeed->value * CL_KeyState(&in_lookup); + cl.viewangles[PITCH] += speed * cl_pitchspeed->value * CL_KeyState(&in_lookdown); +} + +/* +================ +CL_KeyMove + +Sets the usercmd_t based on key states +================ +*/ +static void CL_KeyMove(usercmd_t *cmd) +{ + int movespeed; + int forward, side, up; + + // + // adjust for speed key / running + // the walking flag is to keep animations consistant + // even during acceleration and develeration + // + if (in_speed.active ^ cl_run->integer) + { + movespeed = 127; + cmd->buttons &= ~BUTTON_WALKING; + } + else + { + cmd->buttons |= BUTTON_WALKING; + movespeed = 64; + } + + forward = 0; + side = 0; + up = 0; + if (in_strafe.active) + { + side += movespeed * CL_KeyState(&in_right); + side -= movespeed * CL_KeyState(&in_left); + } + + side += movespeed * CL_KeyState(&in_moveright); + side -= movespeed * CL_KeyState(&in_moveleft); + + up += movespeed * CL_KeyState(&in_up); + up -= movespeed * CL_KeyState(&in_down); + + forward += movespeed * CL_KeyState(&in_forward); + forward -= movespeed * CL_KeyState(&in_back); + + cmd->forwardmove = ClampChar(forward); + cmd->rightmove = ClampChar(side); + cmd->upmove = ClampChar(up); +} + +/* +================= +CL_MouseEvent +================= +*/ +void CL_MouseEvent(int dx, int dy, int time) +{ + if (Key_GetCatcher() & KEYCATCH_UI) + { + VM_Call(cls.ui, UI_MOUSE_EVENT, dx, dy); + } + else if (Key_GetCatcher() & KEYCATCH_CGAME) + { + VM_Call(cls.cgame, CG_MOUSE_EVENT, dx, dy); + } + else + { + cl.mouseDx[cl.mouseIndex] += dx; + cl.mouseDy[cl.mouseIndex] += dy; + } +} + +/* +================= +CL_JoystickEvent + +Joystick values stay set until changed +================= +*/ +void CL_JoystickEvent(int axis, int value, int time) +{ + if (axis < 0 || axis >= MAX_JOYSTICK_AXIS) + { + Com_Error(ERR_DROP, "CL_JoystickEvent: bad axis %i", axis); + } + cl.joystickAxis[axis] = value; +} + +/* +================= +CL_JoystickMove +================= +*/ +static void CL_JoystickMove(usercmd_t *cmd) +{ + float anglespeed; + + float yaw = j_yaw->value * cl.joystickAxis[j_yaw_axis->integer]; + float right = j_side->value * cl.joystickAxis[j_side_axis->integer]; + float forward = j_forward->value * cl.joystickAxis[j_forward_axis->integer]; + float pitch = j_pitch->value * cl.joystickAxis[j_pitch_axis->integer]; + float up = j_up->value * cl.joystickAxis[j_up_axis->integer]; + + if (!(in_speed.active ^ cl_run->integer)) + { + cmd->buttons |= BUTTON_WALKING; + } + + if (in_speed.active) + { + anglespeed = 0.001 * cls.frametime * cl_anglespeedkey->value; + } + else + { + anglespeed = 0.001 * cls.frametime; + } + + if (!in_strafe.active) + { + cl.viewangles[YAW] += anglespeed * yaw; + cmd->rightmove = ClampChar(cmd->rightmove + (int)right); + } + else + { + cl.viewangles[YAW] += anglespeed * right; + cmd->rightmove = ClampChar(cmd->rightmove + (int)yaw); + } + + if (in_mlooking) + { + cl.viewangles[PITCH] += anglespeed * forward; + cmd->forwardmove = ClampChar(cmd->forwardmove + (int)pitch); + } + else + { + cl.viewangles[PITCH] += anglespeed * pitch; + cmd->forwardmove = ClampChar(cmd->forwardmove + (int)forward); + } + + cmd->upmove = ClampChar(cmd->upmove + (int)up); +} + +/* +================= +CL_MouseMove +================= +*/ + +static void CL_MouseMove(usercmd_t *cmd) +{ + float mx, my; + + // allow mouse smoothing + if (m_filter->integer) + { + mx = (cl.mouseDx[0] + cl.mouseDx[1]) * 0.5f; + my = (cl.mouseDy[0] + cl.mouseDy[1]) * 0.5f; + } + else + { + mx = cl.mouseDx[cl.mouseIndex]; + my = cl.mouseDy[cl.mouseIndex]; + } + + cl.mouseIndex ^= 1; + cl.mouseDx[cl.mouseIndex] = 0; + cl.mouseDy[cl.mouseIndex] = 0; + + if (mx == 0.0f && my == 0.0f) return; + + if (cl_mouseAccel->value != 0.0f) + { + if (cl_mouseAccelStyle->integer == 0) + { + float accelSensitivity; + float rate; + + rate = sqrt(mx * mx + my * my) / (float)frame_msec; + + accelSensitivity = cl_sensitivity->value + rate * cl_mouseAccel->value; + mx *= accelSensitivity; + my *= accelSensitivity; + + if (cl_showMouseRate->integer) Com_Printf("rate: %f, accelSensitivity: %f\n", rate, accelSensitivity); + } + else + { + float rate[2]; + float power[2]; + + // sensitivity remains pretty much unchanged at low speeds + // cl_mouseAccel is a power value to how the acceleration is shaped + // cl_mouseAccelOffset is the rate for which the acceleration will have doubled the non accelerated + // amplification + // NOTE: decouple the config cvars for independent acceleration setup along X and Y? + + rate[0] = fabs(mx) / (float)frame_msec; + rate[1] = fabs(my) / (float)frame_msec; + power[0] = powf(rate[0] / cl_mouseAccelOffset->value, cl_mouseAccel->value); + power[1] = powf(rate[1] / cl_mouseAccelOffset->value, cl_mouseAccel->value); + + mx = cl_sensitivity->value * (mx + ((mx < 0) ? -power[0] : power[0]) * cl_mouseAccelOffset->value); + my = cl_sensitivity->value * (my + ((my < 0) ? -power[1] : power[1]) * cl_mouseAccelOffset->value); + + if (cl_showMouseRate->integer) + Com_Printf("ratex: %f, ratey: %f, powx: %f, powy: %f\n", rate[0], rate[1], power[0], power[1]); + } + } + else + { + mx *= cl_sensitivity->value; + my *= cl_sensitivity->value; + } + + // ingame FOV + mx *= cl.cgameSensitivity; + my *= cl.cgameSensitivity; + + // add mouse X/Y movement to cmd + if (in_strafe.active) + cmd->rightmove = ClampChar(cmd->rightmove + m_side->value * mx); + else + cl.viewangles[YAW] -= m_yaw->value * mx; + + if ((in_mlooking || cl_freelook->integer) && !in_strafe.active) + cl.viewangles[PITCH] += m_pitch->value * my; + else + cmd->forwardmove = ClampChar(cmd->forwardmove - m_forward->value * my); +} + +/* +============== +CL_CmdButtons +============== +*/ +static void CL_CmdButtons(usercmd_t *cmd) +{ + int i; + + // + // figure button bits + // send a button bit even if the key was pressed and released in + // less than a frame + // + for (i = 0; i < 15; i++) + { + if (in_buttons[i].active || in_buttons[i].wasPressed) + { + cmd->buttons |= 1 << i; + } + in_buttons[i].wasPressed = false; + } + + if (Key_GetCatcher()) + { + cmd->buttons |= BUTTON_TALK; + } + + // allow the game to know if any key at all is + // currently pressed, even if it isn't bound to anything + if (anykeydown && Key_GetCatcher() == 0) + { + cmd->buttons |= BUTTON_ANY; + } +} + +/* +============== +CL_FinishMove +============== +*/ +static void CL_FinishMove(usercmd_t *cmd) +{ + int i; + + // copy the state that the cgame is currently sending + cmd->weapon = cl.cgameUserCmdValue; + + // send the current server time so the amount of movement + // can be determined without allowing cheating + cmd->serverTime = cl.serverTime; + + for (i = 0; i < 3; i++) + { + cmd->angles[i] = ANGLE2SHORT(cl.viewangles[i]); + } +} + +/* +================= +CL_CreateCmd +================= +*/ +static usercmd_t CL_CreateCmd(void) +{ + usercmd_t cmd; + vec3_t oldAngles; + + VectorCopy(cl.viewangles, oldAngles); + + // keyboard angle adjustment + CL_AdjustAngles(); + + ::memset(&cmd, 0, sizeof(cmd)); + + CL_CmdButtons(&cmd); + + // get basic movement from keyboard + CL_KeyMove(&cmd); + + // get basic movement from mouse + CL_MouseMove(&cmd); + + // get basic movement from joystick + CL_JoystickMove(&cmd); + + // check to make sure the angles haven't wrapped + if (cl.viewangles[PITCH] - oldAngles[PITCH] > 90) + { + cl.viewangles[PITCH] = oldAngles[PITCH] + 90; + } + else if (oldAngles[PITCH] - cl.viewangles[PITCH] > 90) + { + cl.viewangles[PITCH] = oldAngles[PITCH] - 90; + } + + // store out the final values + CL_FinishMove(&cmd); + + // draw debug graphs of turning for mouse testing + if (cl_debugMove->integer) + { + if (cl_debugMove->integer == 1) + { + SCR_DebugGraph(fabs(cl.viewangles[YAW] - oldAngles[YAW])); + } + if (cl_debugMove->integer == 2) + { + SCR_DebugGraph(fabs(cl.viewangles[PITCH] - oldAngles[PITCH])); + } + } + + return cmd; +} + +/* +================= +CL_CreateNewCommands + +Create a new usercmd_t structure for this frame +================= +*/ +static void CL_CreateNewCommands(void) +{ + int cmdNum; + + // no need to create usercmds until we have a gamestate + if (clc.state < CA_PRIMED) + { + return; + } + + frame_msec = com_frameTime - old_com_frameTime; + + // if running over 1000fps, act as if each frame is 1ms + // prevents divisions by zero + if (frame_msec < 1) + { + frame_msec = 1; + } + + // if running less than 5fps, truncate the extra time to prevent + // unexpected moves after a hitch + if (frame_msec > 200) + { + frame_msec = 200; + } + old_com_frameTime = com_frameTime; + + // generate a command for this frame + cl.cmdNumber++; + cmdNum = cl.cmdNumber & CMD_MASK; + cl.cmds[cmdNum] = CL_CreateCmd(); +} + +/* +================= +CL_ReadyToSendPacket + +Returns false if we are over the maxpackets limit +and should choke back the bandwidth a bit by not sending +a packet this frame. All the commands will still get +delivered in the next packet, but saving a header and +getting more delta compression will reduce total bandwidth. +================= +*/ +static bool CL_ReadyToSendPacket(void) +{ + int oldPacketNum; + int delta; + + // don't send anything if playing back a demo + if (clc.demoplaying || clc.state == CA_CINEMATIC) + { + return false; + } + + // If we are downloading, we send no less than 50ms between packets + if (*clc.downloadTempName && cls.realtime - clc.lastPacketSentTime < 50) + { + return false; + } + + // if we don't have a valid gamestate yet, only send + // one packet a second + if (clc.state != CA_ACTIVE && clc.state != CA_PRIMED && !*clc.downloadTempName && + cls.realtime - clc.lastPacketSentTime < 1000) + { + return false; + } + + // send every frame for loopbacks + if (clc.netchan.remoteAddress.type == NA_LOOPBACK) + { + return true; + } + + // send every frame for LAN + if (cl_lanForcePackets->integer && Sys_IsLANAddress(clc.netchan.remoteAddress)) + { + return true; + } + + // check for exceeding cl_maxpackets + if (cl_maxpackets->integer < 15) + { + Cvar_Set("cl_maxpackets", "15"); + } + else if (cl_maxpackets->integer > 125) + { + Cvar_Set("cl_maxpackets", "125"); + } + oldPacketNum = (clc.netchan.outgoingSequence - 1) & PACKET_MASK; + delta = cls.realtime - cl.outPackets[oldPacketNum].p_realtime; + if (delta < 1000 / cl_maxpackets->integer) + { + // the accumulated commands will go out in the next packet + return false; + } + + return true; +} + +/* +=================== +CL_WritePacket + +Create and send the command packet to the server +Including both the reliable commands and the usercmds + +During normal gameplay, a client packet will contain something like: + +4 sequence number +2 qport +4 serverid +4 acknowledged sequence number +4 clc.serverCommandSequence +<optional reliable commands> +1 clc_move or clc_moveNoDelta +1 command count +<count * usercmds> + +=================== +*/ +void CL_WritePacket(void) +{ + msg_t buf; + byte data[MAX_MSGLEN]; + int i, j; + usercmd_t *cmd, *oldcmd; + usercmd_t nullcmd; + int packetNum; + int oldPacketNum; + int count, key; + + // don't send anything if playing back a demo + if (clc.demoplaying || clc.state == CA_CINEMATIC) + { + return; + } + + ::memset(&nullcmd, 0, sizeof(nullcmd)); + oldcmd = &nullcmd; + + MSG_Init(&buf, data, sizeof(data)); + + MSG_Bitstream(&buf); + // write the current serverId so the server + // can tell if this is from the current gameState + MSG_WriteLong(&buf, cl.serverId); + + // write the last message we received, which can + // be used for delta compression, and is also used + // to tell if we dropped a gamestate + MSG_WriteLong(&buf, clc.serverMessageSequence); + + // write the last reliable message we received + MSG_WriteLong(&buf, clc.serverCommandSequence); + + // write any unacknowledged clientCommands + for (i = clc.reliableAcknowledge + 1; i <= clc.reliableSequence; i++) + { + MSG_WriteByte(&buf, clc_clientCommand); + MSG_WriteLong(&buf, i); + MSG_WriteString(&buf, clc.reliableCommands[i & (MAX_RELIABLE_COMMANDS - 1)]); + } + + // we want to send all the usercmds that were generated in the last + // few packet, so even if a couple packets are dropped in a row, + // all the cmds will make it to the server + if (cl_packetdup->integer < 0) + { + Cvar_Set("cl_packetdup", "0"); + } + else if (cl_packetdup->integer > 5) + { + Cvar_Set("cl_packetdup", "5"); + } + oldPacketNum = (clc.netchan.outgoingSequence - 1 - cl_packetdup->integer) & PACKET_MASK; + count = cl.cmdNumber - cl.outPackets[oldPacketNum].p_cmdNumber; + if (count > MAX_PACKET_USERCMDS) + { + count = MAX_PACKET_USERCMDS; + Com_Printf("MAX_PACKET_USERCMDS\n"); + } + +#ifdef USE_VOIP + if (clc.voipOutgoingDataSize > 0) + { + if ((clc.voipFlags & VOIP_SPATIAL) || Com_IsVoipTarget(clc.voipTargets, sizeof(clc.voipTargets), -1)) + { + if (clc.netchan.alternateProtocol != 0) + { + MSG_WriteByte(&buf, clc_EOF); + } + MSG_WriteByte(&buf, clc_voipSpeex); + if (clc.netchan.alternateProtocol != 0) + { + MSG_WriteByte(&buf, clc_voipSpeex + 1); + } + MSG_WriteByte(&buf, clc.voipOutgoingGeneration); + MSG_WriteLong(&buf, clc.voipOutgoingSequence); + MSG_WriteByte(&buf, clc.voipOutgoingDataFrames); + if (clc.netchan.alternateProtocol == 0) + { + MSG_WriteData(&buf, clc.voipTargets, sizeof(clc.voipTargets)); + MSG_WriteByte(&buf, clc.voipFlags); + } + else + { + MSG_WriteLong(&buf, clc.voipTargets[0] | (clc.voipTargets[1] << 8) | (clc.voipTargets[2] << 16) | + ((clc.voipTargets[3] & 0x7F) << 24)); + MSG_WriteLong(&buf, (clc.voipTargets[3] >> 7) | (clc.voipTargets[4] << 1) | (clc.voipTargets[5] << 9) | + (clc.voipTargets[6] << 17) | ((clc.voipTargets[7] & 0x3F) << 25)); + MSG_WriteLong(&buf, clc.voipTargets[7] >> 6); + } + MSG_WriteShort(&buf, clc.voipOutgoingDataSize); + MSG_WriteData(&buf, clc.voipOutgoingData, clc.voipOutgoingDataSize); + + // If we're recording a demo, we have to fake a server packet with + // this VoIP data so it gets to disk; the server doesn't send it + // back to us, and we might as well eliminate concerns about dropped + // and misordered packets here. + if (clc.demorecording && !clc.demowaiting) + { + const int voipSize = clc.voipOutgoingDataSize; + msg_t fakemsg; + byte fakedata[MAX_MSGLEN]; + MSG_Init(&fakemsg, fakedata, sizeof(fakedata)); + MSG_Bitstream(&fakemsg); + MSG_WriteLong(&fakemsg, clc.reliableAcknowledge); + MSG_WriteByte(&fakemsg, svc_voipOpus); + MSG_WriteShort(&fakemsg, clc.clientNum); + MSG_WriteByte(&fakemsg, clc.voipOutgoingGeneration); + MSG_WriteLong(&fakemsg, clc.voipOutgoingSequence); + MSG_WriteByte(&fakemsg, clc.voipOutgoingDataFrames); + MSG_WriteShort(&fakemsg, clc.voipOutgoingDataSize); + if (clc.netchan.alternateProtocol == 0) + { + MSG_WriteBits(&fakemsg, clc.voipFlags, VOIP_FLAGCNT); + } + MSG_WriteData(&fakemsg, clc.voipOutgoingData, voipSize); + MSG_WriteByte(&fakemsg, svc_EOF); + CL_WriteDemoMessage(&fakemsg, 0); + } + + clc.voipOutgoingSequence += clc.voipOutgoingDataFrames; + clc.voipOutgoingDataSize = 0; + clc.voipOutgoingDataFrames = 0; + } + else + { + // We have data, but no targets. Silently discard all data + clc.voipOutgoingDataSize = 0; + clc.voipOutgoingDataFrames = 0; + } + } +#endif + + if (count >= 1) + { + if (cl_showSend->integer) + { + Com_Printf("(%i)", count); + } + + // begin a client move command + if (cl_nodelta->integer || !cl.snap.valid || clc.demowaiting || clc.serverMessageSequence != cl.snap.messageNum) + { + MSG_WriteByte(&buf, clc_moveNoDelta); + } + else + { + MSG_WriteByte(&buf, clc_move); + } + + // write the command count + MSG_WriteByte(&buf, count); + + // use the checksum feed in the key + key = clc.checksumFeed; + // also use the message acknowledge + key ^= clc.serverMessageSequence; + // also use the last acknowledged server command in the key + key ^= MSG_HashKey(clc.netchan.alternateProtocol, + clc.serverCommands[clc.serverCommandSequence & (MAX_RELIABLE_COMMANDS - 1)], 32); + + // write all the commands, including the predicted command + for (i = 0; i < count; i++) + { + j = (cl.cmdNumber - count + i + 1) & CMD_MASK; + cmd = &cl.cmds[j]; + MSG_WriteDeltaUsercmdKey(&buf, key, oldcmd, cmd); + oldcmd = cmd; + } + } + + // + // deliver the message + // + packetNum = clc.netchan.outgoingSequence & PACKET_MASK; + cl.outPackets[packetNum].p_realtime = cls.realtime; + cl.outPackets[packetNum].p_serverTime = oldcmd->serverTime; + cl.outPackets[packetNum].p_cmdNumber = cl.cmdNumber; + clc.lastPacketSentTime = cls.realtime; + + if (cl_showSend->integer) + { + Com_Printf("%i ", buf.cursize); + } + + CL_Netchan_Transmit(&clc.netchan, &buf); +} + +/* +================= +CL_SendCmd + +Called every frame to builds and sends a command packet to the server. +================= +*/ +void CL_SendCmd(void) +{ + // don't send any message if not connected + if (clc.state < CA_CONNECTED) + { + return; + } + + // don't send commands if paused + if (com_sv_running->integer && sv_paused->integer && cl_paused->integer) + { + return; + } + + // we create commands even if a demo is playing, + CL_CreateNewCommands(); + + // don't send a packet if the last packet was sent too recently + if (!CL_ReadyToSendPacket()) + { + if (cl_showSend->integer) + { + Com_Printf(". "); + } + return; + } + + CL_WritePacket(); +} + +/* +============ +CL_InitInput +============ +*/ +void CL_InitInput(void) +{ + Cmd_AddCommand("centerview", IN_CenterView); + + Cmd_AddCommand("+moveup", IN_UpDown); + Cmd_AddCommand("-moveup", IN_UpUp); + Cmd_AddCommand("+movedown", IN_DownDown); + Cmd_AddCommand("-movedown", IN_DownUp); + Cmd_AddCommand("+left", IN_LeftDown); + Cmd_AddCommand("-left", IN_LeftUp); + Cmd_AddCommand("+right", IN_RightDown); + Cmd_AddCommand("-right", IN_RightUp); + Cmd_AddCommand("+forward", IN_ForwardDown); + Cmd_AddCommand("-forward", IN_ForwardUp); + Cmd_AddCommand("+back", IN_BackDown); + Cmd_AddCommand("-back", IN_BackUp); + Cmd_AddCommand("+lookup", IN_LookupDown); + Cmd_AddCommand("-lookup", IN_LookupUp); + Cmd_AddCommand("+lookdown", IN_LookdownDown); + Cmd_AddCommand("-lookdown", IN_LookdownUp); + Cmd_AddCommand("+strafe", IN_StrafeDown); + Cmd_AddCommand("-strafe", IN_StrafeUp); + Cmd_AddCommand("+moveleft", IN_MoveleftDown); + Cmd_AddCommand("-moveleft", IN_MoveleftUp); + Cmd_AddCommand("+moveright", IN_MoverightDown); + Cmd_AddCommand("-moveright", IN_MoverightUp); + Cmd_AddCommand("+speed", IN_SpeedDown); + Cmd_AddCommand("-speed", IN_SpeedUp); + Cmd_AddCommand("+attack", IN_Button0Down); + Cmd_AddCommand("-attack", IN_Button0Up); + Cmd_AddCommand("+button0", IN_Button0Down); + Cmd_AddCommand("-button0", IN_Button0Up); + Cmd_AddCommand("+button1", IN_Button1Down); + Cmd_AddCommand("-button1", IN_Button1Up); + Cmd_AddCommand("+button2", IN_Button2Down); + Cmd_AddCommand("-button2", IN_Button2Up); + Cmd_AddCommand("+button3", IN_Button3Down); + Cmd_AddCommand("-button3", IN_Button3Up); + Cmd_AddCommand("+button4", IN_Button4Down); + Cmd_AddCommand("-button4", IN_Button4Up); + Cmd_AddCommand("+button5", IN_Button5Down); + Cmd_AddCommand("-button5", IN_Button5Up); + Cmd_AddCommand("+button6", IN_Button6Down); + Cmd_AddCommand("-button6", IN_Button6Up); + Cmd_AddCommand("+button7", IN_Button7Down); + Cmd_AddCommand("-button7", IN_Button7Up); + Cmd_AddCommand("+button8", IN_Button8Down); + Cmd_AddCommand("-button8", IN_Button8Up); + Cmd_AddCommand("+button9", IN_Button9Down); + Cmd_AddCommand("-button9", IN_Button9Up); + Cmd_AddCommand("+button10", IN_Button10Down); + Cmd_AddCommand("-button10", IN_Button10Up); + Cmd_AddCommand("+button11", IN_Button11Down); + Cmd_AddCommand("-button11", IN_Button11Up); + Cmd_AddCommand("+button12", IN_Button12Down); + Cmd_AddCommand("-button12", IN_Button12Up); + Cmd_AddCommand("+button13", IN_Button13Down); + Cmd_AddCommand("-button13", IN_Button13Up); + Cmd_AddCommand("+button14", IN_Button14Down); + Cmd_AddCommand("-button14", IN_Button14Up); + Cmd_AddCommand("+mlook", IN_MLookDown); + Cmd_AddCommand("-mlook", IN_MLookUp); + +#ifdef USE_VOIP + Cmd_AddCommand("+voiprecord", IN_VoipRecordDown); + Cmd_AddCommand("-voiprecord", IN_VoipRecordUp); +#endif + + cl_nodelta = Cvar_Get("cl_nodelta", "0", 0); + cl_debugMove = Cvar_Get("cl_debugMove", "0", 0); +} + +/* +============ +CL_ShutdownInput +============ +*/ +void CL_ShutdownInput(void) +{ + Cmd_RemoveCommand("centerview"); + + Cmd_RemoveCommand("+moveup"); + Cmd_RemoveCommand("-moveup"); + Cmd_RemoveCommand("+movedown"); + Cmd_RemoveCommand("-movedown"); + Cmd_RemoveCommand("+left"); + Cmd_RemoveCommand("-left"); + Cmd_RemoveCommand("+right"); + Cmd_RemoveCommand("-right"); + Cmd_RemoveCommand("+forward"); + Cmd_RemoveCommand("-forward"); + Cmd_RemoveCommand("+back"); + Cmd_RemoveCommand("-back"); + Cmd_RemoveCommand("+lookup"); + Cmd_RemoveCommand("-lookup"); + Cmd_RemoveCommand("+lookdown"); + Cmd_RemoveCommand("-lookdown"); + Cmd_RemoveCommand("+strafe"); + Cmd_RemoveCommand("-strafe"); + Cmd_RemoveCommand("+moveleft"); + Cmd_RemoveCommand("-moveleft"); + Cmd_RemoveCommand("+moveright"); + Cmd_RemoveCommand("-moveright"); + Cmd_RemoveCommand("+speed"); + Cmd_RemoveCommand("-speed"); + Cmd_RemoveCommand("+attack"); + Cmd_RemoveCommand("-attack"); + Cmd_RemoveCommand("+button0"); + Cmd_RemoveCommand("-button0"); + Cmd_RemoveCommand("+button1"); + Cmd_RemoveCommand("-button1"); + Cmd_RemoveCommand("+button2"); + Cmd_RemoveCommand("-button2"); + Cmd_RemoveCommand("+button3"); + Cmd_RemoveCommand("-button3"); + Cmd_RemoveCommand("+button4"); + Cmd_RemoveCommand("-button4"); + Cmd_RemoveCommand("+button5"); + Cmd_RemoveCommand("-button5"); + Cmd_RemoveCommand("+button6"); + Cmd_RemoveCommand("-button6"); + Cmd_RemoveCommand("+button7"); + Cmd_RemoveCommand("-button7"); + Cmd_RemoveCommand("+button8"); + Cmd_RemoveCommand("-button8"); + Cmd_RemoveCommand("+button9"); + Cmd_RemoveCommand("-button9"); + Cmd_RemoveCommand("+button10"); + Cmd_RemoveCommand("-button10"); + Cmd_RemoveCommand("+button11"); + Cmd_RemoveCommand("-button11"); + Cmd_RemoveCommand("+button12"); + Cmd_RemoveCommand("-button12"); + Cmd_RemoveCommand("+button13"); + Cmd_RemoveCommand("-button13"); + Cmd_RemoveCommand("+button14"); + Cmd_RemoveCommand("-button14"); + Cmd_RemoveCommand("+mlook"); + Cmd_RemoveCommand("-mlook"); + +#ifdef USE_VOIP + Cmd_RemoveCommand("+voiprecord"); + Cmd_RemoveCommand("-voiprecord"); +#endif +} diff --git a/src/client/cl_keys.cpp b/src/client/cl_keys.cpp new file mode 100644 index 0000000..82f5666 --- /dev/null +++ b/src/client/cl_keys.cpp @@ -0,0 +1,1665 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#include "client.h" + +/* + +key up events are sent even if in console mode + +*/ + +field_t historyEditLines[COMMAND_HISTORY]; + +int nextHistoryLine; // the last line in the history buffer, not masked +int historyLine; // the line being displayed from history buffer will be <= nextHistoryLine + +field_t g_consoleField; + +field_t chatField; +bool chat_team; +bool chat_admins; +bool chat_clans; +int chat_playerNum; + +bool key_overstrikeMode; + +int anykeydown; +qkey_t keys[MAX_KEYS]; + +struct keyname_t { + const char* name; + int keynum; +}; + +// names not in this list can either be lowercase ascii, or '0xnn' hex sequences +keyname_t keynames[] = +{ + {"TAB", K_TAB}, + {"ENTER", K_ENTER}, + {"ESCAPE", K_ESCAPE}, + {"SPACE", K_SPACE}, + {"BACKSPACE", K_BACKSPACE}, + {"UPARROW", K_UPARROW}, + {"DOWNARROW", K_DOWNARROW}, + {"LEFTARROW", K_LEFTARROW}, + {"RIGHTARROW", K_RIGHTARROW}, + + {"ALT", K_ALT}, + {"CTRL", K_CTRL}, + {"SHIFT", K_SHIFT}, + + {"COMMAND", K_COMMAND}, + + {"CAPSLOCK", K_CAPSLOCK}, + + {"F1", K_F1}, + {"F2", K_F2}, + {"F3", K_F3}, + {"F4", K_F4}, + {"F5", K_F5}, + {"F6", K_F6}, + {"F7", K_F7}, + {"F8", K_F8}, + {"F9", K_F9}, + {"F10", K_F10}, + {"F11", K_F11}, + {"F12", K_F12}, + {"F13", K_F13}, + {"F14", K_F14}, + {"F15", K_F15}, + + {"INS", K_INS}, + {"DEL", K_DEL}, + {"PGDN", K_PGDN}, + {"PGUP", K_PGUP}, + {"HOME", K_HOME}, + {"END", K_END}, + + {"MOUSE1", K_MOUSE1}, + {"MOUSE2", K_MOUSE2}, + {"MOUSE3", K_MOUSE3}, + {"MOUSE4", K_MOUSE4}, + {"MOUSE5", K_MOUSE5}, + + {"MWHEELUP", K_MWHEELUP }, + {"MWHEELDOWN", K_MWHEELDOWN }, + + {"JOY1", K_JOY1}, + {"JOY2", K_JOY2}, + {"JOY3", K_JOY3}, + {"JOY4", K_JOY4}, + {"JOY5", K_JOY5}, + {"JOY6", K_JOY6}, + {"JOY7", K_JOY7}, + {"JOY8", K_JOY8}, + {"JOY9", K_JOY9}, + {"JOY10", K_JOY10}, + {"JOY11", K_JOY11}, + {"JOY12", K_JOY12}, + {"JOY13", K_JOY13}, + {"JOY14", K_JOY14}, + {"JOY15", K_JOY15}, + {"JOY16", K_JOY16}, + {"JOY17", K_JOY17}, + {"JOY18", K_JOY18}, + {"JOY19", K_JOY19}, + {"JOY20", K_JOY20}, + {"JOY21", K_JOY21}, + {"JOY22", K_JOY22}, + {"JOY23", K_JOY23}, + {"JOY24", K_JOY24}, + {"JOY25", K_JOY25}, + {"JOY26", K_JOY26}, + {"JOY27", K_JOY27}, + {"JOY28", K_JOY28}, + {"JOY29", K_JOY29}, + {"JOY30", K_JOY30}, + {"JOY31", K_JOY31}, + {"JOY32", K_JOY32}, + + {"AUX1", K_AUX1}, + {"AUX2", K_AUX2}, + {"AUX3", K_AUX3}, + {"AUX4", K_AUX4}, + {"AUX5", K_AUX5}, + {"AUX6", K_AUX6}, + {"AUX7", K_AUX7}, + {"AUX8", K_AUX8}, + {"AUX9", K_AUX9}, + {"AUX10", K_AUX10}, + {"AUX11", K_AUX11}, + {"AUX12", K_AUX12}, + {"AUX13", K_AUX13}, + {"AUX14", K_AUX14}, + {"AUX15", K_AUX15}, + {"AUX16", K_AUX16}, + + {"KP_HOME", K_KP_HOME }, + {"KP_UPARROW", K_KP_UPARROW }, + {"KP_PGUP", K_KP_PGUP }, + {"KP_LEFTARROW", K_KP_LEFTARROW }, + {"KP_5", K_KP_5 }, + {"KP_RIGHTARROW", K_KP_RIGHTARROW }, + {"KP_END", K_KP_END }, + {"KP_DOWNARROW", K_KP_DOWNARROW }, + {"KP_PGDN", K_KP_PGDN }, + {"KP_ENTER", K_KP_ENTER }, + {"KP_INS", K_KP_INS }, + {"KP_DEL", K_KP_DEL }, + {"KP_SLASH", K_KP_SLASH }, + {"KP_MINUS", K_KP_MINUS }, + {"KP_PLUS", K_KP_PLUS }, + {"KP_NUMLOCK", K_KP_NUMLOCK }, + {"KP_STAR", K_KP_STAR }, + {"KP_EQUALS", K_KP_EQUALS }, + + {"PAUSE", K_PAUSE}, + + {"SEMICOLON", ';'}, // because a raw semicolon seperates commands + + {"WORLD_0", K_WORLD_0}, + {"WORLD_1", K_WORLD_1}, + {"WORLD_2", K_WORLD_2}, + {"WORLD_3", K_WORLD_3}, + {"WORLD_4", K_WORLD_4}, + {"WORLD_5", K_WORLD_5}, + {"WORLD_6", K_WORLD_6}, + {"WORLD_7", K_WORLD_7}, + {"WORLD_8", K_WORLD_8}, + {"WORLD_9", K_WORLD_9}, + {"WORLD_10", K_WORLD_10}, + {"WORLD_11", K_WORLD_11}, + {"WORLD_12", K_WORLD_12}, + {"WORLD_13", K_WORLD_13}, + {"WORLD_14", K_WORLD_14}, + {"WORLD_15", K_WORLD_15}, + {"WORLD_16", K_WORLD_16}, + {"WORLD_17", K_WORLD_17}, + {"WORLD_18", K_WORLD_18}, + {"WORLD_19", K_WORLD_19}, + {"WORLD_20", K_WORLD_20}, + {"WORLD_21", K_WORLD_21}, + {"WORLD_22", K_WORLD_22}, + {"WORLD_23", K_WORLD_23}, + {"WORLD_24", K_WORLD_24}, + {"WORLD_25", K_WORLD_25}, + {"WORLD_26", K_WORLD_26}, + {"WORLD_27", K_WORLD_27}, + {"WORLD_28", K_WORLD_28}, + {"WORLD_29", K_WORLD_29}, + {"WORLD_30", K_WORLD_30}, + {"WORLD_31", K_WORLD_31}, + {"WORLD_32", K_WORLD_32}, + {"WORLD_33", K_WORLD_33}, + {"WORLD_34", K_WORLD_34}, + {"WORLD_35", K_WORLD_35}, + {"WORLD_36", K_WORLD_36}, + {"WORLD_37", K_WORLD_37}, + {"WORLD_38", K_WORLD_38}, + {"WORLD_39", K_WORLD_39}, + {"WORLD_40", K_WORLD_40}, + {"WORLD_41", K_WORLD_41}, + {"WORLD_42", K_WORLD_42}, + {"WORLD_43", K_WORLD_43}, + {"WORLD_44", K_WORLD_44}, + {"WORLD_45", K_WORLD_45}, + {"WORLD_46", K_WORLD_46}, + {"WORLD_47", K_WORLD_47}, + {"WORLD_48", K_WORLD_48}, + {"WORLD_49", K_WORLD_49}, + {"WORLD_50", K_WORLD_50}, + {"WORLD_51", K_WORLD_51}, + {"WORLD_52", K_WORLD_52}, + {"WORLD_53", K_WORLD_53}, + {"WORLD_54", K_WORLD_54}, + {"WORLD_55", K_WORLD_55}, + {"WORLD_56", K_WORLD_56}, + {"WORLD_57", K_WORLD_57}, + {"WORLD_58", K_WORLD_58}, + {"WORLD_59", K_WORLD_59}, + {"WORLD_60", K_WORLD_60}, + {"WORLD_61", K_WORLD_61}, + {"WORLD_62", K_WORLD_62}, + {"WORLD_63", K_WORLD_63}, + {"WORLD_64", K_WORLD_64}, + {"WORLD_65", K_WORLD_65}, + {"WORLD_66", K_WORLD_66}, + {"WORLD_67", K_WORLD_67}, + {"WORLD_68", K_WORLD_68}, + {"WORLD_69", K_WORLD_69}, + {"WORLD_70", K_WORLD_70}, + {"WORLD_71", K_WORLD_71}, + {"WORLD_72", K_WORLD_72}, + {"WORLD_73", K_WORLD_73}, + {"WORLD_74", K_WORLD_74}, + {"WORLD_75", K_WORLD_75}, + {"WORLD_76", K_WORLD_76}, + {"WORLD_77", K_WORLD_77}, + {"WORLD_78", K_WORLD_78}, + {"WORLD_79", K_WORLD_79}, + {"WORLD_80", K_WORLD_80}, + {"WORLD_81", K_WORLD_81}, + {"WORLD_82", K_WORLD_82}, + {"WORLD_83", K_WORLD_83}, + {"WORLD_84", K_WORLD_84}, + {"WORLD_85", K_WORLD_85}, + {"WORLD_86", K_WORLD_86}, + {"WORLD_87", K_WORLD_87}, + {"WORLD_88", K_WORLD_88}, + {"WORLD_89", K_WORLD_89}, + {"WORLD_90", K_WORLD_90}, + {"WORLD_91", K_WORLD_91}, + {"WORLD_92", K_WORLD_92}, + {"WORLD_93", K_WORLD_93}, + {"WORLD_94", K_WORLD_94}, + {"WORLD_95", K_WORLD_95}, + + {"WINDOWS", K_SUPER}, + {"COMPOSE", K_COMPOSE}, + {"MODE", K_MODE}, + {"HELP", K_HELP}, + {"PRINT", K_PRINT}, + {"SYSREQ", K_SYSREQ}, + {"SCROLLOCK", K_SCROLLOCK }, + {"BREAK", K_BREAK}, + {"MENU", K_MENU}, + {"POWER", K_POWER}, + {"EURO", K_EURO}, + {"UNDO", K_UNDO}, + + {"PAD0_A", K_PAD0_A }, + {"PAD0_B", K_PAD0_B }, + {"PAD0_X", K_PAD0_X }, + {"PAD0_Y", K_PAD0_Y }, + {"PAD0_BACK", K_PAD0_BACK }, + {"PAD0_GUIDE", K_PAD0_GUIDE }, + {"PAD0_START", K_PAD0_START }, + {"PAD0_LEFTSTICK_CLICK", K_PAD0_LEFTSTICK_CLICK }, + {"PAD0_RIGHTSTICK_CLICK", K_PAD0_RIGHTSTICK_CLICK }, + {"PAD0_LEFTSHOULDER", K_PAD0_LEFTSHOULDER }, + {"PAD0_RIGHTSHOULDER", K_PAD0_RIGHTSHOULDER }, + {"PAD0_DPAD_UP", K_PAD0_DPAD_UP }, + {"PAD0_DPAD_DOWN", K_PAD0_DPAD_DOWN }, + {"PAD0_DPAD_LEFT", K_PAD0_DPAD_LEFT }, + {"PAD0_DPAD_RIGHT", K_PAD0_DPAD_RIGHT }, + + {"PAD0_LEFTSTICK_LEFT", K_PAD0_LEFTSTICK_LEFT }, + {"PAD0_LEFTSTICK_RIGHT", K_PAD0_LEFTSTICK_RIGHT }, + {"PAD0_LEFTSTICK_UP", K_PAD0_LEFTSTICK_UP }, + {"PAD0_LEFTSTICK_DOWN", K_PAD0_LEFTSTICK_DOWN }, + {"PAD0_RIGHTSTICK_LEFT", K_PAD0_RIGHTSTICK_LEFT }, + {"PAD0_RIGHTSTICK_RIGHT", K_PAD0_RIGHTSTICK_RIGHT }, + {"PAD0_RIGHTSTICK_UP", K_PAD0_RIGHTSTICK_UP }, + {"PAD0_RIGHTSTICK_DOWN", K_PAD0_RIGHTSTICK_DOWN }, + {"PAD0_LEFTTRIGGER", K_PAD0_LEFTTRIGGER }, + {"PAD0_RIGHTTRIGGER", K_PAD0_RIGHTTRIGGER }, + + {NULL,0} +}; + +/* +============================================================================= + +EDIT FIELDS + +============================================================================= +*/ + + +/* +=================== +Field_Draw + +Handles horizontal scrolling and cursor blinking +x, y, and width are in pixels +=================== +*/ +static void Field_VariableSizeDraw( field_t *edit, int x, int y, int width, int size, + bool showCursor, bool noColorEscape ) +{ + int len; + int drawLen; + int prestep; + int cursorChar; + char str[MAX_STRING_CHARS]; + int i; + + drawLen = edit->widthInChars - 1; // - 1 so there is always a space for the cursor + len = strlen( edit->buffer ); + + // guarantee that cursor will be visible + if ( len <= drawLen ) { + prestep = 0; + } else { + if ( edit->scroll + drawLen > len ) { + edit->scroll = len - drawLen; + if ( edit->scroll < 0 ) { + edit->scroll = 0; + } + } + prestep = edit->scroll; + } + + if ( prestep + drawLen > len ) { + drawLen = len - prestep; + } + + // extract <drawLen> characters from the field at <prestep> + if ( drawLen >= MAX_STRING_CHARS ) { + Com_Error( ERR_DROP, "drawLen >= MAX_STRING_CHARS" ); + } + + ::memcpy( str, edit->buffer + prestep, drawLen ); + str[ drawLen ] = 0; + + // draw it + if ( size == SMALLCHAR_WIDTH ) { + float color[4]; + + color[0] = color[1] = color[2] = color[3] = 1.0; + SCR_DrawSmallStringExt( x, y, str, color, false, noColorEscape ); + } else { + // draw big string with drop shadow + SCR_DrawBigString( x, y, str, 1.0, noColorEscape ); + } + + // draw the cursor + if ( showCursor ) { + if ( (int)( cls.realtime >> 8 ) & 1 ) { + return; // off blink + } + + if ( key_overstrikeMode ) { + cursorChar = 11; + } else { + cursorChar = 10; + } + + i = drawLen - strlen( str ); + + if ( size == SMALLCHAR_WIDTH ) { + SCR_DrawSmallChar( x + ( edit->cursor - prestep - i ) * size, y, cursorChar ); + } else { + str[0] = cursorChar; + str[1] = 0; + SCR_DrawBigString( x + ( edit->cursor - prestep - i ) * size, y, str, 1.0, false ); + + } + } +} + +void Field_Draw( field_t *edit, int x, int y, int width, bool showCursor, bool noColorEscape ) +{ + Field_VariableSizeDraw( edit, x, y, width, SMALLCHAR_WIDTH, showCursor, noColorEscape ); +} + +void Field_BigDraw( field_t *edit, int x, int y, int width, bool showCursor, bool noColorEscape ) +{ + Field_VariableSizeDraw( edit, x, y, width, BIGCHAR_WIDTH, showCursor, noColorEscape ); +} + +/* +================ +Field_Paste +================ +*/ +static void Field_Paste( field_t *edit ) +{ + char *cbd; + int pasteLen, i; + + cbd = Sys_GetClipboardData(); + + if ( !cbd ) { + return; + } + + // send as if typed, so insert / overstrike works properly + pasteLen = strlen( cbd ); + for ( i = 0 ; i < pasteLen ; i++ ) { + Field_CharEvent( edit, cbd[i] ); + } + + Z_Free( cbd ); +} + +/* +================= +Field_KeyDownEvent + +Performs the basic line editing functions for the console, +in-game talk, and menu fields + +Key events are used for non-printable characters, others are gotten from char events. +================= +*/ +void Field_KeyDownEvent( field_t *edit, int key ) +{ + int len; + + // shift-insert is paste + if ( ( ( key == K_INS ) || ( key == K_KP_INS ) ) && keys[K_SHIFT].down ) + { + Field_Paste( edit ); + return; + } + + key = tolower( key ); + len = strlen( edit->buffer ); + + switch ( key ) + { + case K_DEL: + if ( edit->cursor < len ) + { + memmove( edit->buffer + edit->cursor, + edit->buffer + edit->cursor + 1, + len - edit->cursor ); + } + break; + + case K_RIGHTARROW: + if ( edit->cursor < len ) { + edit->cursor++; + } + break; + + case K_LEFTARROW: + if ( edit->cursor > 0 ) { + edit->cursor--; + } + break; + + case K_HOME: + edit->cursor = 0; + break; + + case K_END: + edit->cursor = len; + break; + + case K_INS: + key_overstrikeMode = !key_overstrikeMode; + break; + + default: + break; + } + + // Change scroll if cursor is no longer visible + if ( edit->cursor < edit->scroll ) + { + edit->scroll = edit->cursor; + } + else if ( edit->cursor >= edit->scroll + edit->widthInChars && edit->cursor <= len ) + { + edit->scroll = edit->cursor - edit->widthInChars + 1; + } +} + +/* +================== +Field_CharEvent +================== +*/ +void Field_CharEvent( field_t *edit, int ch ) +{ + int len; + + if ( ch == 'v' - 'a' + 1 ) { // ctrl-v is paste + Field_Paste( edit ); + return; + } + + if ( ch == 'c' - 'a' + 1 ) { // ctrl-c clears the field + Field_Clear( edit ); + return; + } + + len = strlen( edit->buffer ); + + if ( ch == 'h' - 'a' + 1 ) { // ctrl-h is backspace + if ( edit->cursor > 0 ) { + memmove( edit->buffer + edit->cursor - 1, + edit->buffer + edit->cursor, len + 1 - edit->cursor ); + edit->cursor--; + if ( edit->cursor < edit->scroll ) + { + edit->scroll--; + } + } + return; + } + + if ( ch == 'a' - 'a' + 1 ) { // ctrl-a is home + edit->cursor = 0; + edit->scroll = 0; + return; + } + + if ( ch == 'e' - 'a' + 1 ) { // ctrl-e is end + edit->cursor = len; + edit->scroll = edit->cursor - edit->widthInChars; + return; + } + + // + // ignore any other non printable chars + // + if ( ch < 32 ) { + return; + } + + if ( key_overstrikeMode ) { + // - 2 to leave room for the leading slash and trailing \0 + if ( edit->cursor == MAX_EDIT_LINE - 2 ) + return; + edit->buffer[edit->cursor] = ch; + edit->cursor++; + } else { // insert mode + // - 2 to leave room for the leading slash and trailing \0 + if ( len == MAX_EDIT_LINE - 2 ) { + return; // all full + } + memmove( edit->buffer + edit->cursor + 1, + edit->buffer + edit->cursor, len + 1 - edit->cursor ); + edit->buffer[edit->cursor] = ch; + edit->cursor++; + } + + + if ( edit->cursor >= edit->widthInChars ) { + edit->scroll++; + } + + if ( edit->cursor == len + 1) { + edit->buffer[edit->cursor] = 0; + } +} + +/* +============================================================================= + +CONSOLE LINE EDITING + +============================================================================== +*/ + +/* +==================== +Console_Key + +Handles history and console scrollback +==================== +*/ +static void Console_Key(int key) +{ + // ctrl-L clears screen + if ( key == 'l' && keys[K_CTRL].down ) + { + Cbuf_AddText ("clear\n"); + return; + } + + // enter finishes the line + if ( key == K_ENTER || key == K_KP_ENTER ) + { + // if not in the game explicitly prepend a slash if needed + if ( clc.state != CA_ACTIVE + && g_consoleField.buffer[0] + && g_consoleField.buffer[0] != '\\' + && g_consoleField.buffer[0] != '/' ) + { + char temp[MAX_EDIT_LINE-1]; + + Q_strncpyz( temp, g_consoleField.buffer, sizeof( temp ) ); + Com_sprintf( g_consoleField.buffer, sizeof( g_consoleField.buffer ), "\\%s", temp ); + g_consoleField.cursor++; + } + + Com_Printf ( "]%s\n", g_consoleField.buffer ); + + // leading slash is an explicit command + if ( g_consoleField.buffer[0] == '\\' || g_consoleField.buffer[0] == '/' ) + { + Cbuf_AddText( g_consoleField.buffer+1 ); // valid command + Cbuf_AddText ("\n"); + } + else + { + // other text will be chat messages + if ( !g_consoleField.buffer[0] ) + { + return; // empty lines just scroll the console without adding to history + } + else + { + Cbuf_AddText ("cmd say "); + Cbuf_AddText( g_consoleField.buffer ); + Cbuf_AddText ("\n"); + } + } + + // copy line to history buffer + historyEditLines[nextHistoryLine % COMMAND_HISTORY] = g_consoleField; + nextHistoryLine++; + historyLine = nextHistoryLine; + + Field_Clear( &g_consoleField ); + + g_consoleField.widthInChars = g_console_field_width; + + CL_SaveConsoleHistory( ); + + if ( clc.state == CA_DISCONNECTED ) { + SCR_UpdateScreen (); // force an update, because the command + } // may take some time + return; + } + + // command completion + + if (key == K_TAB) { + Field_AutoComplete(&g_consoleField); + return; + } + + // command history (ctrl-p ctrl-n for unix style) + + if ( (key == K_MWHEELUP && keys[K_SHIFT].down) || ( key == K_UPARROW ) || ( key == K_KP_UPARROW ) || + ( ( tolower(key) == 'p' ) && keys[K_CTRL].down ) ) { + if ( nextHistoryLine - historyLine < COMMAND_HISTORY + && historyLine > 0 ) { + historyLine--; + } + g_consoleField = historyEditLines[ historyLine % COMMAND_HISTORY ]; + return; + } + + if ( (key == K_MWHEELDOWN && keys[K_SHIFT].down) || ( key == K_DOWNARROW ) || ( key == K_KP_DOWNARROW ) || + ( ( tolower(key) == 'n' ) && keys[K_CTRL].down ) ) { + historyLine++; + if (historyLine >= nextHistoryLine) { + historyLine = nextHistoryLine; + Field_Clear( &g_consoleField ); + g_consoleField.widthInChars = g_console_field_width; + return; + } + g_consoleField = historyEditLines[ historyLine % COMMAND_HISTORY ]; + return; + } + + // console scrolling + if ( key == K_PGUP ) { + Con_PageUp(); + return; + } + + if ( key == K_PGDN) { + Con_PageDown(); + return; + } + + if ( key == K_MWHEELUP) { //----(SA) added some mousewheel functionality to the console + Con_PageUp(); + if(keys[K_CTRL].down) { // hold <ctrl> to accelerate scrolling + Con_PageUp(); + Con_PageUp(); + } + return; + } + + if ( key == K_MWHEELDOWN) { //----(SA) added some mousewheel functionality to the console + Con_PageDown(); + if(keys[K_CTRL].down) { // hold <ctrl> to accelerate scrolling + Con_PageDown(); + Con_PageDown(); + } + return; + } + + // ctrl-home = top of console + if ( key == K_HOME && keys[K_CTRL].down ) { + Con_Top(); + return; + } + + // ctrl-end = bottom of console + if ( key == K_END && keys[K_CTRL].down ) { + Con_Bottom(); + return; + } + + // pass to the normal editline routine + Field_KeyDownEvent( &g_consoleField, key ); +} +//============================================================================ + + + +/* +================= +Message_Key + +In game talk message +================= +*/ +static void Message_Key( int key ) { + + char buffer[MAX_STRING_CHARS]; + + if (key == K_ESCAPE) { + Key_SetCatcher( Key_GetCatcher( ) & ~KEYCATCH_MESSAGE ); + Field_Clear( &chatField); + return; + } + + if ( key == K_ENTER || key == K_KP_ENTER ) { + if ( chatField.buffer[0] && clc.state == CA_ACTIVE ){ + + if( chatField.buffer[0] == '/' || + chatField.buffer[0] == '\\' ) + { + Com_sprintf( buffer, sizeof( buffer ), "%s\n", &chatField.buffer[1] ); + } + + else if (chat_playerNum != -1 ) { + Com_sprintf( buffer, sizeof( buffer ), + "tell %i \"%s\"\n", + chat_playerNum, + chatField.buffer ); + } + else if (chat_team) { + Com_sprintf( buffer, sizeof( buffer ), + "say_team \"%s\"\n", + chatField.buffer ); + } + else if (chat_admins) { + Com_sprintf( buffer, sizeof( buffer ), + "a \"%s\"\n", + chatField.buffer ); + } + else if (chat_clans) { + char clantagDecolored[ 32 ]; + + Q_strncpyz( clantagDecolored, cl_clantag->string, + sizeof(clantagDecolored) ); + Q_CleanStr( clantagDecolored ); + + if( strlen(clantagDecolored) > 2 && strlen(clantagDecolored) < 11 ) { + Com_sprintf( buffer, sizeof( buffer ), + "m \"%s\" \"%s\"\n", clantagDecolored, + chatField.buffer ); + } else { + //string isnt long enough + Com_Printf ( + "^3Error:your cl_clantag has to be Between 3 and 10 chars long. current value is:^7 %s^7\n", + clantagDecolored ); + return; + } + } + else { + Com_sprintf( buffer, sizeof( buffer ), + "say \"%s\"\n", chatField.buffer ); + } + + CL_AddReliableCommand( buffer, false ); + } + Key_SetCatcher( Key_GetCatcher( ) & ~KEYCATCH_MESSAGE ); + Field_Clear( &chatField ); + return; + } + + Field_KeyDownEvent( &chatField, key); +} + +//============================================================================ + + +bool Key_GetOverstrikeMode( void ) +{ + return key_overstrikeMode; +} + + +void Key_SetOverstrikeMode( bool state ) +{ + key_overstrikeMode = state; +} + + +/* +=================== +Key_IsDown +=================== +*/ +bool Key_IsDown( int keynum ) +{ + if ( keynum < 0 || keynum >= MAX_KEYS ) + return false; + + return keys[keynum].down; +} + +/* +=================== +Key_StringToKeynum + +Returns a key number to be used to index keys[] by looking at +the given string. Single ascii characters return themselves, while +the K_* names are matched up. + +0x11 will be interpreted as raw hex, which will allow new controlers + +to be configured even if they don't have defined names. +=================== +*/ +int Key_StringToKeynum( const char *str ) { + keyname_t *kn; + + if ( !str || !str[0] ) { + return -1; + } + if ( !str[1] ) { + return tolower( str[0] ); + } + + // check for hex code + if ( strlen( str ) == 4 ) { + int n = Com_HexStrToInt( str ); + + if ( n >= 0 ) { + return n; + } + } + + // scan for a text match + for ( kn=keynames ; kn->name ; kn++ ) { + if ( !Q_stricmp( str,kn->name ) ) + return kn->keynum; + } + + return -1; +} + +/* +=================== +Key_KeynumToString + +Returns a string (either a single ascii char, a K_* name, or a 0x11 hex string) for the +given keynum. +=================== +*/ +const char *Key_KeynumToString( int keynum ) { + keyname_t *kn; + static char tinystr[5]; + int i, j; + + if ( keynum == -1 ) { + return "<KEY NOT FOUND>"; + } + + if ( keynum < 0 || keynum >= MAX_KEYS ) { + return "<OUT OF RANGE>"; + } + + // check for printable ascii (don't use quote) + if ( keynum > 32 && keynum < 127 && keynum != '"' && keynum != ';' ) { + tinystr[0] = keynum; + tinystr[1] = 0; + return tinystr; + } + + // check for a key string + for ( kn=keynames ; kn->name ; kn++ ) { + if (keynum == kn->keynum) { + return kn->name; + } + } + + // make a hex string + i = keynum >> 4; + j = keynum & 15; + + tinystr[0] = '0'; + tinystr[1] = 'x'; + tinystr[2] = i > 9 ? i - 10 + 'a' : i + '0'; + tinystr[3] = j > 9 ? j - 10 + 'a' : j + '0'; + tinystr[4] = 0; + + return tinystr; +} + + +/* +=================== +Key_SetBinding +=================== +*/ +void Key_SetBinding( int keynum, const char *binding ) { + if ( keynum < 0 || keynum >= MAX_KEYS ) { + return; + } + + // free old bindings + if ( keys[ keynum ].binding ) { + Z_Free( keys[ keynum ].binding ); + } + + // allocate memory for new binding + keys[keynum].binding = CopyString( binding ); + + // consider this like modifying an archived cvar, so the + // file write will be triggered at the next oportunity + cvar_modifiedFlags |= CVAR_ARCHIVE; +} + + +/* +=================== +Key_GetBinding +=================== +*/ +const char *Key_GetBinding( int keynum ) { + if ( keynum < 0 || keynum >= MAX_KEYS ) { + return ""; + } + + return keys[ keynum ].binding; +} + +/* +=================== +Key_GetKey +=================== +*/ + +int Key_GetKey(const char *binding) { + int i; + + if (binding) { + for (i=0 ; i < MAX_KEYS ; i++) { + if (keys[i].binding && Q_stricmp(binding, keys[i].binding) == 0) { + return i; + } + } + } + return -1; +} + +/* +=================== +Key_Unbind_f +=================== +*/ +void Key_Unbind_f (void) +{ + if (Cmd_Argc() != 2) + { + Com_Printf ("unbind <key> : remove commands from a key\n"); + return; + } + + int b = Key_StringToKeynum(Cmd_Argv(1)); + if ( b == -1 ) + { + Com_Printf ("\"%s\" isn't a valid key\n", Cmd_Argv(1)); + return; + } + + Key_SetBinding (b, ""); +} + +/* +=================== +Key_Unbindall_f +=================== +*/ +void Key_Unbindall_f (void) +{ + int i; + + for (i=0 ; i < MAX_KEYS; i++) + if (keys[i].binding) + Key_SetBinding (i, ""); +} + + +/* +=================== +Key_Bind_f +=================== +*/ +void Key_Bind_f (void) +{ + int i, c, b; + char cmd[1024]; + + c = Cmd_Argc(); + + if (c < 2) + { + Com_Printf ("bind <key> [command] : attach a command to a key\n"); + return; + } + b = Key_StringToKeynum (Cmd_Argv(1)); + if (b==-1) + { + Com_Printf ("\"%s\" isn't a valid key\n", Cmd_Argv(1)); + return; + } + + if (c == 2) + { + if (keys[b].binding && keys[b].binding[0]) + Com_Printf ("\"%s\" = \"%s\"\n", Key_KeynumToString(b), keys[b].binding ); + else + Com_Printf ("\"%s\" is not bound\n", Key_KeynumToString(b) ); + return; + } + +// copy the rest of the command line + cmd[0] = 0; // start out with a null string + for (i=2 ; i< c ; i++) + { + strcat (cmd, Cmd_Argv(i)); + if (i != (c-1)) + strcat (cmd, " "); + } + + Key_SetBinding (b, cmd); +} + +/* +============ +Key_WriteBindings + +Writes lines containing "bind key value" +============ +*/ +void Key_WriteBindings( fileHandle_t f ) { + int i; + + FS_Printf (f, "unbindall\n" ); + + for (i=0 ; i<MAX_KEYS ; i++) { + if (keys[i].binding && keys[i].binding[0] ) { + FS_Printf (f, "bind %s \"%s\"\n", Key_KeynumToString(i), keys[i].binding); + + } + + } +} + + +/* +============ +Key_Bindlist_f + +============ +*/ +void Key_Bindlist_f( void ) { + int i; + + for ( i = 0 ; i < MAX_KEYS ; i++ ) { + if ( keys[i].binding && keys[i].binding[0] ) { + Com_Printf( "%s \"%s\"\n", Key_KeynumToString(i), keys[i].binding ); + } + } +} + +/* +============ +Key_KeynameCompletion +============ +*/ +void Key_KeynameCompletion( void(*callback)(const char *s) ) { + int i; + + for( i = 0; keynames[ i ].name != NULL; i++ ) + callback( keynames[ i ].name ); +} + +/* +==================== +Key_CompleteUnbind +==================== +*/ +static void Key_CompleteUnbind( char *args, int argNum ) +{ + if( argNum == 2 ) + { + // Skip "unbind " + char *p = Com_SkipTokens( args, 1, " " ); + + if( p > args ) + Field_CompleteKeyname( ); + } +} + +/* +==================== +Key_CompleteBind +==================== +*/ +static void Key_CompleteBind( char *args, int argNum ) +{ + char *p; + + if( argNum == 2 ) + { + // Skip "bind " + p = Com_SkipTokens( args, 1, " " ); + + if( p > args ) + Field_CompleteKeyname( ); + } + else if( argNum >= 3 ) + { + // Skip "bind <key> " + p = Com_SkipTokens( args, 2, " " ); + + if( p > args ) + Field_CompleteCommand( p, true, true ); + } +} + +/* +=================== +CL_InitKeyCommands +=================== +*/ +void CL_InitKeyCommands( void ) +{ + // register our functions + Cmd_AddCommand ("bind",Key_Bind_f); + Cmd_SetCommandCompletionFunc( "bind", Key_CompleteBind ); + Cmd_AddCommand ("unbind",Key_Unbind_f); + Cmd_SetCommandCompletionFunc( "unbind", Key_CompleteUnbind ); + Cmd_AddCommand ("unbindall",Key_Unbindall_f); + Cmd_AddCommand ("bindlist",Key_Bindlist_f); +} + +/* +=================== +CL_BindUICommand + +Returns true if bind command should be executed while user interface is shown +=================== +*/ +static bool CL_BindUICommand( const char *cmd ) +{ + if ( Key_GetCatcher( ) & KEYCATCH_CONSOLE ) + return false; + + if ( !Q_stricmp( cmd, "toggleconsole" ) ) + return true; + if ( !Q_stricmp( cmd, "togglemenu" ) ) + return true; + + return false; +} + +/* +=================== +CL_ParseBinding + +Execute the commands in the bind string +=================== +*/ +static void CL_ParseBinding( int key, bool down, unsigned time ) +{ + char buf[ MAX_STRING_CHARS ], *p = buf, *end; + bool allCommands, allowUpCmds; + + if( clc.state == CA_DISCONNECTED && Key_GetCatcher( ) == 0 ) + return; + if( !keys[key].binding || !keys[key].binding[0] ) + return; + Q_strncpyz( buf, keys[key].binding, sizeof( buf ) ); + + // run all bind commands if console, ui, etc aren't reading keys + allCommands = ( Key_GetCatcher( ) == 0 ); + + // allow button up commands if in game even if key catcher is set + allowUpCmds = ( clc.state != CA_DISCONNECTED ); + + while( 1 ) + { + while( isspace( *p ) ) + p++; + end = strchr( p, ';' ); + if( end ) + *end = '\0'; + if( *p == '+' ) + { + // button commands add keynum and time as parameters + // so that multiple sources can be discriminated and + // subframe corrected + if ( allCommands || ( allowUpCmds && !down ) ) { + char cmd[1024]; + Com_sprintf( cmd, sizeof( cmd ), "%c%s %d %d\n", + ( down ) ? '+' : '-', p + 1, key, time ); + Cbuf_AddText( cmd ); + } + } + else if( down ) + { + // normal commands only execute on key press + if ( allCommands || CL_BindUICommand( p ) ) { + Cbuf_AddText( p ); + Cbuf_AddText( "\n" ); + } + } + if( !end ) + break; + p = end + 1; + } +} + +/* +=================== +CL_KeyDownEvent + +Called by CL_KeyEvent to handle a keypress +=================== +*/ +static void CL_KeyDownEvent( int key, unsigned time ) +{ + keys[key].down = true; + keys[key].repeats++; + if( keys[key].repeats == 1 ) + anykeydown++; + + if( keys[K_ALT].down && key == K_ENTER ) + { + Cvar_SetValue( "r_fullscreen", + !Cvar_VariableIntegerValue( "r_fullscreen" ) ); + return; + } + + // console key is hardcoded, so the user can never unbind it + if( key == K_CONSOLE || ( keys[K_SHIFT].down && key == K_ESCAPE ) ) + { + Con_ToggleConsole_f (); + Key_ClearStates (); + return; + } + + + // keys can still be used for bound actions + if ( ( key < 128 || key == K_MOUSE1 ) && + ( clc.demoplaying || clc.state == CA_CINEMATIC ) && Key_GetCatcher( ) == 0 ) { + + if (Cvar_VariableValue ("com_cameraMode") == 0) { + if( clc.demoplaying && key != K_ESCAPE ) { + // avoid accidental stopping of demos from pressing a random key by opening the console + Con_ToggleConsole_f (); + Key_ClearStates (); + return; + } + Cvar_Set ("nextdemo",""); + key = K_ESCAPE; + } + } + + // escape is always handled special + if ( key == K_ESCAPE ) { + if ( clc.netchan.alternateProtocol == 2 && + ( Key_GetCatcher( ) & KEYCATCH_MESSAGE ) ) { + // clear message mode + Message_Key( key ); + return; + } + + // escape always gets out of CGAME stuff + if (Key_GetCatcher( ) & KEYCATCH_CGAME) { + Key_SetCatcher( Key_GetCatcher( ) & ~KEYCATCH_CGAME ); + VM_Call (cls.cgame, CG_EVENT_HANDLING, CGAME_EVENT_NONE); + return; + } + + if ( !( Key_GetCatcher( ) & KEYCATCH_UI ) ) { + if ( clc.state == CA_ACTIVE && !clc.demoplaying ) { + VM_Call( cls.ui, UI_SET_ACTIVE_MENU - ( cls.uiInterface == 2 ? 2 : 0 ), UIMENU_INGAME ); + } + else if ( clc.state != CA_DISCONNECTED ) { + CL_Disconnect_f(); + S_StopAllSounds(); + VM_Call( cls.ui, UI_SET_ACTIVE_MENU - ( cls.uiInterface == 2 ? 2 : 0 ), UIMENU_MAIN ); + } + return; + } + + VM_Call( cls.ui, UI_KEY_EVENT, key, true ); + return; + } + + // send the bound action + CL_ParseBinding( key, true, time ); + + // distribute the key down event to the apropriate handler + if ( Key_GetCatcher( ) & KEYCATCH_CONSOLE ) { + Console_Key( key ); + } else if ( Key_GetCatcher( ) & KEYCATCH_UI ) { + if ( cls.ui ) { + VM_Call( cls.ui, UI_KEY_EVENT, key, true ); + } + } else if ( Key_GetCatcher( ) & KEYCATCH_CGAME ) { + if ( cls.cgame ) { + VM_Call( cls.cgame, CG_KEY_EVENT, key, true ); + } + } else if ( clc.netchan.alternateProtocol == 2 && + ( Key_GetCatcher( ) & KEYCATCH_MESSAGE ) ) { + Message_Key( key ); + } else if ( clc.state == CA_DISCONNECTED ) { + Console_Key( key ); + } +} + +/* +=================== +CL_KeyUpEvent + +Called by CL_KeyEvent to handle a keyrelease +=================== +*/ +static void CL_KeyUpEvent( int key, unsigned time ) +{ + keys[key].repeats = 0; + keys[key].down = false; + anykeydown--; + + if (anykeydown < 0) { + anykeydown = 0; + } + + // don't process key-up events for the console key + if ( key == K_CONSOLE || ( key == K_ESCAPE && keys[K_SHIFT].down ) ) + return; + + // + // key up events only perform actions if the game key binding is + // a button command (leading + sign). These will be processed even in + // console mode and menu mode, to keep the character from continuing + // an action started before a mode switch. + // + CL_ParseBinding( key, false, time ); + + if ( Key_GetCatcher( ) & KEYCATCH_UI && cls.ui ) { + VM_Call( cls.ui, UI_KEY_EVENT, key, false ); + } else if ( Key_GetCatcher( ) & KEYCATCH_CGAME && cls.cgame ) { + VM_Call( cls.cgame, CG_KEY_EVENT, key, false ); + } +} + +/* +=================== +CL_KeyEvent + +Called by the system for both key up and key down events +=================== +*/ +void CL_KeyEvent(int key, bool down, unsigned time) +{ + if( down ) + CL_KeyDownEvent( key, time ); + else + CL_KeyUpEvent( key, time ); +} + +/* +=================== +CL_CharEvent + +Normal keyboard characters, already shifted / capslocked / etc +=================== +*/ +void CL_CharEvent( int key ) +{ + // delete is not a printable character and is otherwise handled by + // Field_KeyDownEvent + if ( key == 127 ) + return; + + // distribute the key down event to the apropriate handler + if ( Key_GetCatcher( ) & KEYCATCH_CONSOLE ) + Field_CharEvent( &g_consoleField, key ); + + else if ( Key_GetCatcher( ) & KEYCATCH_UI ) + VM_Call( cls.ui, UI_KEY_EVENT, key | K_CHAR_FLAG, true ); + + else if ( clc.netchan.alternateProtocol == 2 && + ( Key_GetCatcher( ) & KEYCATCH_MESSAGE ) ) + Field_CharEvent( &chatField, key ); + + else if ( clc.state == CA_DISCONNECTED ) + Field_CharEvent( &g_consoleField, key ); +} + + +/* +=================== +Key_ClearStates +=================== +*/ +void Key_ClearStates(void) +{ + anykeydown = 0; + + for ( int i = 0 ; i < MAX_KEYS ; i++ ) + { + if ( keys[i].down ) + CL_KeyEvent( i, false, 0 ); + + keys[i].down = 0; + keys[i].repeats = 0; + } +} + +/* +==================== +Key_KeynumToStringBuf +==================== +*/ +void Key_KeynumToStringBuf( int keynum, char *buf, int buflen ) +{ + Q_strncpyz( buf, Key_KeynumToString( keynum ), buflen ); +} + +/* +==================== +Key_GetBindingBuf +==================== +*/ +void Key_GetBindingBuf( int keynum, char *buf, int buflen ) +{ + const char* value = Key_GetBinding( keynum ); + if ( value ) + { + Q_strncpyz( buf, value, buflen ); + } + else + { + *buf = 0; + } +} + +static int keyCatchers = 0; + +/* +==================== +Key_GetCatcher +==================== +*/ +int Key_GetCatcher( void ) +{ + return keyCatchers; +} + +/* +==================== +Key_SetCatcher +==================== +*/ +void Key_SetCatcher( int catcher ) +{ + // If the catcher state is changing, clear all key states + if( catcher != keyCatchers ) + Key_ClearStates( ); + + keyCatchers = catcher; +} + +// This must not exceed MAX_CMD_LINE +#define MAX_CONSOLE_SAVE_BUFFER 1024 +#define CONSOLE_HISTORY_FILE "q3history" +static char consoleSaveBuffer[ MAX_CONSOLE_SAVE_BUFFER ]; +static int consoleSaveBufferSize = 0; + +/* +================ +CL_LoadConsoleHistory + +Load the console history from cl_consoleHistory +================ +*/ +void CL_LoadConsoleHistory( void ) +{ + char *token, *text_p; + int i, numChars, numLines = 0; + fileHandle_t f; + + consoleSaveBufferSize = FS_FOpenFileRead( CONSOLE_HISTORY_FILE, &f, false ); + if( !f ) + { + Com_Printf( "Couldn't read %s.\n", CONSOLE_HISTORY_FILE ); + return; + } + + if( consoleSaveBufferSize <= MAX_CONSOLE_SAVE_BUFFER && + FS_Read( consoleSaveBuffer, consoleSaveBufferSize, f ) == consoleSaveBufferSize ) + { + text_p = consoleSaveBuffer; + + for( i = COMMAND_HISTORY - 1; i >= 0; i-- ) + { + if( !*( token = COM_Parse( &text_p ) ) ) + break; + + historyEditLines[ i ].cursor = atoi( token ); + + if( !*( token = COM_Parse( &text_p ) ) ) + break; + + historyEditLines[ i ].scroll = atoi( token ); + + if( !*( token = COM_Parse( &text_p ) ) ) + break; + + numChars = atoi( token ); + text_p++; + if( numChars > ( strlen( consoleSaveBuffer ) - ( text_p - consoleSaveBuffer ) ) ) + { + Com_DPrintf( S_COLOR_YELLOW "WARNING: probable corrupt history\n" ); + break; + } + ::memcpy( historyEditLines[ i ].buffer, text_p, numChars ); + historyEditLines[ i ].buffer[ numChars ] = '\0'; + text_p += numChars; + + numLines++; + } + + memmove( &historyEditLines[ 0 ], &historyEditLines[ i + 1 ], + numLines * sizeof( field_t ) ); + for( i = numLines; i < COMMAND_HISTORY; i++ ) + Field_Clear( &historyEditLines[ i ] ); + + historyLine = nextHistoryLine = numLines; + } + else + Com_Printf( "Couldn't read %s.\n", CONSOLE_HISTORY_FILE ); + + FS_FCloseFile( f ); +} + +/* +================ +CL_SaveConsoleHistory + +Save the console history into the cvar cl_consoleHistory +so that it persists across invocations of q3 +================ +*/ +void CL_SaveConsoleHistory( void ) +{ + int i; + int lineLength, saveBufferLength, additionalLength; + fileHandle_t f; + + consoleSaveBuffer[ 0 ] = '\0'; + + i = ( nextHistoryLine - 1 ) % COMMAND_HISTORY; + do + { + if( historyEditLines[ i ].buffer[ 0 ] ) + { + lineLength = strlen( historyEditLines[ i ].buffer ); + saveBufferLength = strlen( consoleSaveBuffer ); + + //ICK + additionalLength = lineLength + strlen( "999 999 999 " ); + + if( saveBufferLength + additionalLength < MAX_CONSOLE_SAVE_BUFFER ) + { + Q_strcat( consoleSaveBuffer, MAX_CONSOLE_SAVE_BUFFER, + va( "%d %d %d %s ", + historyEditLines[ i ].cursor, + historyEditLines[ i ].scroll, + lineLength, + historyEditLines[ i ].buffer ) ); + } + else + break; + } + i = ( i - 1 + COMMAND_HISTORY ) % COMMAND_HISTORY; + } + while( i != ( nextHistoryLine - 1 ) % COMMAND_HISTORY ); + + consoleSaveBufferSize = strlen( consoleSaveBuffer ); + + f = FS_FOpenFileWrite( CONSOLE_HISTORY_FILE ); + if( !f ) + { + Com_Printf( "Couldn't write %s.\n", CONSOLE_HISTORY_FILE ); + return; + } + + if( FS_Write( consoleSaveBuffer, consoleSaveBufferSize, f ) < consoleSaveBufferSize ) + Com_Printf( "Couldn't write %s.\n", CONSOLE_HISTORY_FILE ); + + FS_FCloseFile( f ); +} diff --git a/src/client/cl_main.cpp b/src/client/cl_main.cpp new file mode 100644 index 0000000..8b33acd --- /dev/null +++ b/src/client/cl_main.cpp @@ -0,0 +1,5083 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ +// cl_main.c -- client main loop + +#include "client.h" + +#ifndef _WIN32 +#include <sys/stat.h> +#endif + +#include <climits> + +#include "sys/sys_loadlib.h" +#include "sys/sys_local.h" + +#include "cl_updates.h" +#ifdef USE_MUMBLE +#include "libmumblelink.h" +#endif + +#ifdef USE_MUMBLE +cvar_t *cl_useMumble; +cvar_t *cl_mumbleScale; +#endif + +#ifdef USE_VOIP +cvar_t *cl_voipUseVAD; +cvar_t *cl_voipVADThreshold; +cvar_t *cl_voipSend; +cvar_t *cl_voipSendTarget; +cvar_t *cl_voipGainDuringCapture; +cvar_t *cl_voipCaptureMult; +cvar_t *cl_voipShowMeter; +cvar_t *cl_voipProtocol; +cvar_t *cl_voip; +#endif + +#ifdef USE_RENDERER_DLOPEN +cvar_t *cl_renderer; +#endif + +cvar_t *cl_nodelta; +cvar_t *cl_debugMove; + +cvar_t *cl_noprint; +cvar_t *cl_motd; + +cvar_t *rcon_client_password; +cvar_t *rconAddress; + +cvar_t *cl_timeout; +cvar_t *cl_maxpackets; +cvar_t *cl_packetdup; +cvar_t *cl_timeNudge; +cvar_t *cl_showTimeDelta; +cvar_t *cl_freezeDemo; + +cvar_t *cl_shownet; +cvar_t *cl_showSend; +cvar_t *cl_timedemo; +cvar_t *cl_timedemoLog; +cvar_t *cl_autoRecordDemo; +cvar_t *cl_aviFrameRate; +cvar_t *cl_aviMotionJpeg; +cvar_t *cl_forceavidemo; + +cvar_t *cl_freelook; +cvar_t *cl_sensitivity; + +cvar_t *cl_mouseAccel; +cvar_t *cl_mouseAccelOffset; +cvar_t *cl_mouseAccelStyle; +cvar_t *cl_showMouseRate; + +cvar_t *m_pitch; +cvar_t *m_yaw; +cvar_t *m_forward; +cvar_t *m_side; +cvar_t *m_filter; + +cvar_t *j_pitch; +cvar_t *j_yaw; +cvar_t *j_forward; +cvar_t *j_side; +cvar_t *j_up; +cvar_t *j_pitch_axis; +cvar_t *j_yaw_axis; +cvar_t *j_forward_axis; +cvar_t *j_side_axis; +cvar_t *j_up_axis; + +cvar_t *cl_activeAction; + +cvar_t *cl_motdString; + +cvar_t *cl_allowDownload; +cvar_t *com_downloadPrompt; +cvar_t *cl_conXOffset; +cvar_t *cl_inGameVideo; + +cvar_t *cl_serverStatusResendTime; + +cvar_t *cl_lanForcePackets; + +cvar_t *cl_guidServerUniq; + +cvar_t *cl_clantag; + +cvar_t *cl_consoleKeys; + +cvar_t *cl_rate; + +cvar_t *cl_rsaAuth; + +clientActive_t cl; +clientConnection_t clc; +clientStatic_t cls; + +char cl_reconnectArgs[MAX_OSPATH]; +char cl_oldGame[MAX_QPATH]; +bool cl_oldGameSet; + +// Structure containing functions exported from refresh DLL +refexport_t re; +#ifdef USE_RENDERER_DLOPEN +static void *rendererLib = NULL; +#endif + +ping_t cl_pinglist[MAX_PINGREQUESTS]; + +typedef struct serverStatus_s { + char string[BIG_INFO_STRING]; + netadr_t address; + int time, startTime; + bool pending; + bool print; + bool retrieved; +} serverStatus_t; + +serverStatus_t cl_serverStatusList[MAX_SERVERSTATUSREQUESTS]; + +static void CL_InitRef(void); + +#if defined __USEA3D && defined __A3D_GEOM +void hA3Dg_ExportRenderGeom(refexport_t *incoming_re); +#endif + +static bool noGameRestart = false; + +static void CL_DownloadUpdate_f() { CL_DownloadRelease(); } + +static void CL_InstallUpdate_f() +{ + if (Cmd_Argc() > 1) + CL_ExecuteInstaller(); + else + CL_ExecuteInstaller(); +} + +static void CL_CheckForUpdate_f() { CL_GetLatestRelease(); } + +static void CL_BrowseHomepath_f() { FS_BrowseHomepath(); } + +static void CL_BrowseDemos_f() { FS_OpenBaseGamePath( "demos/" ); } + +static void CL_BrowseScreenShots_f() { FS_OpenBaseGamePath( "screenshots/" ); } + +#ifdef USE_MUMBLE +static void CL_UpdateMumble(void) +{ + vec3_t pos, forward, up; + float scale = cl_mumbleScale->value; + float tmp; + + if (!cl_useMumble->integer) return; + + // !!! FIXME: not sure if this is even close to correct. + if (clc.netchan.alternateProtocol == 2) + { + AngleVectors(cl.snap.alternatePs.viewangles, forward, NULL, up); + + pos[0] = cl.snap.alternatePs.origin[0] * scale; + pos[1] = cl.snap.alternatePs.origin[2] * scale; + pos[2] = cl.snap.alternatePs.origin[1] * scale; + } + else + { + AngleVectors(cl.snap.ps.viewangles, forward, NULL, up); + + pos[0] = cl.snap.ps.origin[0] * scale; + pos[1] = cl.snap.ps.origin[2] * scale; + pos[2] = cl.snap.ps.origin[1] * scale; + } + + tmp = forward[1]; + forward[1] = forward[2]; + forward[2] = tmp; + + tmp = up[1]; + up[1] = up[2]; + up[2] = tmp; + + if (cl_useMumble->integer > 1) + { + fprintf(stderr, "%f %f %f, %f %f %f, %f %f %f\n", + pos[0], pos[1], pos[2], + forward[0], forward[1], forward[2], + up[0], up[1], up[2]); + } + + mumble_update_coordinates(pos, forward, up); +} +#endif + +#ifdef USE_VOIP +static void CL_UpdateVoipIgnore(const char *idstr, bool ignore) +{ + if ((*idstr >= '0') && (*idstr <= '9')) + { + const int id = atoi(idstr); + if ((id >= 0) && (id < MAX_CLIENTS)) + { + clc.voipIgnore[id] = ignore; + CL_AddReliableCommand(va("voip %s %d", ignore ? "ignore" : "unignore", id), false); + Com_Printf("VoIP: %s ignoring player #%d\n", ignore ? "Now" : "No longer", id); + return; + } + } + Com_Printf("VoIP: invalid player ID#\n"); +} + +static void CL_UpdateVoipGain(const char *idstr, float gain) +{ + if ((*idstr >= '0') && (*idstr <= '9')) + { + const int id = atoi(idstr); + if (gain < 0.0f) gain = 0.0f; + if ((id >= 0) && (id < MAX_CLIENTS)) + { + clc.voipGain[id] = gain; + Com_Printf("VoIP: player #%d gain now set to %f\n", id, gain); + } + } +} + +void CL_Voip_f(void) +{ + const char *cmd = Cmd_Argv(1); + const char *reason = NULL; + + if (clc.state != CA_ACTIVE) + reason = "Not connected to a server"; + else if (!clc.voipCodecInitialized) + reason = "Voip codec not initialized"; + else if (!clc.voipEnabled) + reason = "Server doesn't support VoIP"; + + if (reason != NULL) + { + Com_Printf("VoIP: command ignored: %s\n", reason); + return; + } + + if (strcmp(cmd, "ignore") == 0) + { + CL_UpdateVoipIgnore(Cmd_Argv(2), true); + } + else if (strcmp(cmd, "unignore") == 0) + { + CL_UpdateVoipIgnore(Cmd_Argv(2), false); + } + else if (strcmp(cmd, "gain") == 0) + { + if (Cmd_Argc() > 3) + { + CL_UpdateVoipGain(Cmd_Argv(2), atof(Cmd_Argv(3))); + } + else if (Q_isanumber(Cmd_Argv(2))) + { + int id = atoi(Cmd_Argv(2)); + if (id >= 0 && id < MAX_CLIENTS) + { + Com_Printf( + "VoIP: current gain for player #%d " + "is %f\n", + id, clc.voipGain[id]); + } + else + { + Com_Printf("VoIP: invalid player ID#\n"); + } + } + else + { + Com_Printf("usage: voip gain <playerID#> [value]\n"); + } + } + else if (strcmp(cmd, "muteall") == 0) + { + Com_Printf("VoIP: muting incoming voice\n"); + CL_AddReliableCommand("voip muteall", false); + clc.voipMuteAll = true; + } + else if (strcmp(cmd, "unmuteall") == 0) + { + Com_Printf("VoIP: unmuting incoming voice\n"); + CL_AddReliableCommand("voip unmuteall", false); + clc.voipMuteAll = false; + } + else + { + Com_Printf( + "usage: voip [un]ignore <playerID#>\n" + " voip [un]muteall\n" + " voip gain <playerID#> [value]\n"); + } +} + +static void CL_VoipNewGeneration(void) +{ + // don't have a zero generation so new clients won't match, and don't + // wrap to negative so MSG_ReadLong() doesn't "fail." + clc.voipOutgoingGeneration++; + if (clc.voipOutgoingGeneration <= 0) clc.voipOutgoingGeneration = 1; + clc.voipPower = 0.0f; + clc.voipOutgoingSequence = 0; + + opus_encoder_ctl(clc.opusEncoder, OPUS_RESET_STATE); +} + +/* +=============== +CL_VoipParseTargets + +sets clc.voipTargets according to cl_voipSendTarget +Generally we don't want who's listening to change during a transmission, +so this is only called when the key is first pressed +=============== +*/ +static void CL_VoipParseTargets(void) +{ + const char *target = cl_voipSendTarget->string; + char *end; + int val; + + ::memset(clc.voipTargets, 0, sizeof(clc.voipTargets)); + clc.voipFlags &= ~VOIP_SPATIAL; + + while (target) + { + while (*target == ',' || *target == ' ') target++; + + if (!*target) break; + + if (isdigit(*target)) + { + val = strtol(target, &end, 10); + target = end; + } + else + { + if (!Q_stricmpn(target, "all", 3)) + { + ::memset(clc.voipTargets, ~0, sizeof(clc.voipTargets)); + return; + } + if (!Q_stricmpn(target, "spatial", 7)) + { + clc.voipFlags |= VOIP_SPATIAL; + target += 7; + continue; + } + else + { + if (!Q_stricmpn(target, "attacker", 8)) + { + val = VM_Call(cls.cgame, CG_LAST_ATTACKER); + target += 8; + } + else if (!Q_stricmpn(target, "crosshair", 9)) + { + val = VM_Call(cls.cgame, CG_CROSSHAIR_PLAYER); + target += 9; + } + else + { + while (*target && *target != ',' && *target != ' ') target++; + + continue; + } + + if (val < 0) continue; + } + } + + if (val < 0 || val >= MAX_CLIENTS) + { + Com_Printf(S_COLOR_YELLOW + "WARNING: VoIP " + "target %d is not a valid client " + "number\n", + val); + + continue; + } + + clc.voipTargets[val / 8] |= 1 << (val % 8); + } +} + +/* +=============== +CL_CaptureVoip + +Record more audio from the hardware if required and encode it into Opus + data for later transmission. +=============== +*/ +static void CL_CaptureVoip(void) +{ + const float audioMult = cl_voipCaptureMult->value; + const bool useVad = (cl_voipUseVAD->integer != 0); + bool initialFrame = false; + bool finalFrame = false; + +#if USE_MUMBLE + // if we're using Mumble, don't try to handle VoIP transmission ourselves. + if (cl_useMumble->integer) return; +#endif + + // If your data rate is too low, you'll get Connection Interrupted warnings + // when VoIP packets arrive, even if you have a broadband connection. + // This might work on rates lower than 25000, but for safety's sake, we'll + // just demand it. Who doesn't have at least a DSL line now, anyhow? If + // you don't, you don't need VoIP. :) + if (cl_voip->modified || cl_rate->modified) + { + if ((cl_voip->integer) && (cl_rate->integer < 25000)) + { + Com_Printf(S_COLOR_YELLOW "Your network rate is too slow for VoIP.\n"); + Com_Printf("Set 'Data Rate' to 'LAN/Cable/xDSL' in 'Setup/System/Network'.\n"); + Com_Printf("Until then, VoIP is disabled.\n"); + Cvar_Set("cl_voip", "0"); + } + Cvar_Set("cl_voipProtocol", cl_voip->integer ? "opus" : ""); + cl_voip->modified = false; + cl_rate->modified = false; + } + + if (!clc.voipCodecInitialized) return; // just in case this gets called at a bad time. + + if (clc.voipOutgoingDataSize > 0) return; // packet is pending transmission, don't record more yet. + + if (cl_voipUseVAD->modified) + { + Cvar_Set("cl_voipSend", (useVad) ? "1" : "0"); + cl_voipUseVAD->modified = false; + } + + if ((useVad) && (!cl_voipSend->integer)) Cvar_Set("cl_voipSend", "1"); // lots of things reset this. + + if (cl_voipSend->modified) + { + bool dontCapture = false; + if (clc.state != CA_ACTIVE) + dontCapture = true; // not connected to a server. + else if (!clc.voipEnabled) + dontCapture = true; // server doesn't support VoIP. + else if (clc.demoplaying) + dontCapture = true; // playing back a demo. + else if (cl_voip->integer == 0) + dontCapture = true; // client has VoIP support disabled. + else if (audioMult == 0.0f) + dontCapture = true; // basically silenced incoming audio. + + cl_voipSend->modified = false; + + if (dontCapture) + { + Cvar_Set("cl_voipSend", "0"); + return; + } + + if (cl_voipSend->integer) + { + initialFrame = true; + } + else + { + finalFrame = true; + } + } + + // try to get more audio data from the sound card... + + if (initialFrame) + { + S_MasterGain(Com_Clamp(0.0f, 1.0f, cl_voipGainDuringCapture->value)); + S_StartCapture(); + CL_VoipNewGeneration(); + CL_VoipParseTargets(); + } + + if ((cl_voipSend->integer) || (finalFrame)) + { // user wants to capture audio? + int samples = S_AvailableCaptureSamples(); + const int packetSamples = (finalFrame) ? VOIP_MAX_FRAME_SAMPLES : VOIP_MAX_PACKET_SAMPLES; + + // enough data buffered in audio hardware to process yet? + if (samples >= packetSamples) + { + // audio capture is always MONO16. + static int16_t sampbuffer[VOIP_MAX_PACKET_SAMPLES]; + float voipPower = 0.0f; + int voipFrames; + int i, bytes; + + if (samples > VOIP_MAX_PACKET_SAMPLES) samples = VOIP_MAX_PACKET_SAMPLES; + + // !!! FIXME: maybe separate recording from encoding, so voipPower + // !!! FIXME: updates faster than 4Hz? + + samples -= samples % VOIP_MAX_FRAME_SAMPLES; + if (samples != 120 && samples != 240 && samples != 480 && samples != 960 && samples != 1920 && + samples != 2880) + { + Com_Printf("Voip: bad number of samples %d\n", samples); + return; + } + voipFrames = samples / VOIP_MAX_FRAME_SAMPLES; + + S_Capture(samples, (byte *)sampbuffer); // grab from audio card. + + // check the "power" of this packet... + for (i = 0; i < samples; i++) + { + const float flsamp = (float)sampbuffer[i]; + const float s = fabs(flsamp); + voipPower += s * s; + sampbuffer[i] = (int16_t)((flsamp)*audioMult); + } + + // encode raw audio samples into Opus data... + bytes = opus_encode(clc.opusEncoder, sampbuffer, samples, (unsigned char *)clc.voipOutgoingData, + sizeof(clc.voipOutgoingData)); + if (bytes <= 0) + { + Com_DPrintf("VoIP: Error encoding %d samples\n", samples); + bytes = 0; + } + + clc.voipPower = (voipPower / (32768.0f * 32768.0f * ((float)samples))) * 100.0f; + + if ((useVad) && (clc.voipPower < cl_voipVADThreshold->value)) + { + CL_VoipNewGeneration(); // no "talk" for at least 1/4 second. + } + else + { + clc.voipOutgoingDataSize = bytes; + clc.voipOutgoingDataFrames = voipFrames; + + Com_DPrintf("VoIP: Send %d frames, %d bytes, %f power\n", voipFrames, bytes, clc.voipPower); + +#if 0 + static FILE *encio = NULL; + if (encio == NULL) encio = fopen("voip-outgoing-encoded.bin", "wb"); + if (encio != NULL) { fwrite(clc.voipOutgoingData, bytes, 1, encio); fflush(encio); } + static FILE *decio = NULL; + if (decio == NULL) decio = fopen("voip-outgoing-decoded.bin", "wb"); + if (decio != NULL) { fwrite(sampbuffer, voipFrames * VOIP_MAX_FRAME_SAMPLES * 2, 1, decio); fflush(decio); } +#endif + } + } + } + + // User requested we stop recording, and we've now processed the last of + // any previously-buffered data. Pause the capture device, etc. + if (finalFrame) + { + S_StopCapture(); + S_MasterGain(1.0f); + clc.voipPower = 0.0f; // force this value so it doesn't linger. + } +} +#endif + +/* +======================================================================= + +CLIENT RELIABLE COMMAND COMMUNICATION + +======================================================================= +*/ + +/* +====================== +CL_AddReliableCommand + +The given command will be transmitted to the server, and is gauranteed to +not have future usercmd_t executed before it is executed +====================== +*/ +void CL_AddReliableCommand(const char *cmd, bool isDisconnectCmd) +{ + int unacknowledged = clc.reliableSequence - clc.reliableAcknowledge; + + // if we would be losing an old command that hasn't been acknowledged, + // we must drop the connection + // also leave one slot open for the disconnect command in this case. + + if ((isDisconnectCmd && unacknowledged > MAX_RELIABLE_COMMANDS) || + (!isDisconnectCmd && unacknowledged >= MAX_RELIABLE_COMMANDS)) + { + if (com_errorEntered) + return; + else + Com_Error(ERR_DROP, "Client command overflow"); + } + + Q_strncpyz( + clc.reliableCommands[++clc.reliableSequence & (MAX_RELIABLE_COMMANDS - 1)], cmd, sizeof(*clc.reliableCommands)); +} + +/* +======================================================================= + +CLIENT SIDE DEMO RECORDING + +======================================================================= +*/ + +/* +==================== +CL_WriteDemoMessage + +Dumps the current net message, prefixed by the length +==================== +*/ +void CL_WriteDemoMessage(msg_t *msg, int headerBytes) +{ + int len, swlen; + + // write the packet sequence + len = clc.serverMessageSequence; + swlen = LittleLong(len); + FS_Write(&swlen, 4, clc.demofile); + // skip the packet sequencing information + len = msg->cursize - headerBytes; + swlen = LittleLong(len); + FS_Write(&swlen, 4, clc.demofile); + FS_Write(msg->data + headerBytes, len, clc.demofile); +} + +/* +==================== +CL_StopRecording_f + +stop recording a demo +==================== +*/ +void CL_StopRecord_f(void) +{ + int len; + + if (!clc.demorecording) + { + Com_Printf("Not recording a demo.\n"); + return; + } + + // finish up + len = -1; + FS_Write(&len, 4, clc.demofile); + FS_Write(&len, 4, clc.demofile); + FS_FCloseFile(clc.demofile); + clc.demofile = 0; + clc.demorecording = false; + clc.spDemoRecording = false; + Com_Printf("Stopped demo.\n"); +} + +/* +================== +CL_DemoFilename +================== +*/ +static void CL_DemoFilename(int number, char *fileName, int fileNameSize) +{ + int a, b, c, d; + + if (number < 0 || number > 9999) number = 9999; + + a = number / 1000; + number -= a * 1000; + b = number / 100; + number -= b * 100; + c = number / 10; + number -= c * 10; + d = number; + + Com_sprintf(fileName, fileNameSize, "demo%i%i%i%i", a, b, c, d); +} + +/* +==================== +CL_Record_f + +record <demoname> + +Begins recording a demo from the current position +==================== +*/ +static char demoName[MAX_QPATH]; // compiler bug workaround + +static void CL_Record_f(void) +{ + char name[MAX_OSPATH]; + byte bufData[MAX_MSGLEN]; + msg_t buf; + int i; + int len; + entityState_t *ent; + entityState_t nullstate; + + if (Cmd_Argc() > 2) + { + Com_Printf("record <demoname>\n"); + return; + } + + if (clc.demorecording) + { + if (!clc.spDemoRecording) + { + Com_Printf("Already recording.\n"); + } + return; + } + + if (clc.state != CA_ACTIVE) + { + Com_Printf("You must be in a level to record.\n"); + return; + } + + // sync 0 doesn't prevent recording, so not forcing it off .. everyone does g_sync 1 ; record ; g_sync 0 .. + if (NET_IsLocalAddress(clc.serverAddress) && !Cvar_VariableValue("g_synchronousClients")) + { + Com_Printf(S_COLOR_YELLOW "WARNING: You should set 'g_synchronousClients 1' for smoother demo recording\n"); + } + + if (Cmd_Argc() == 2) + { + const char *s = Cmd_Argv(1); + Q_strncpyz(demoName, s, sizeof(demoName)); + Com_sprintf( + name, sizeof(name), + "demos/%s.%s%d", demoName, DEMOEXT, + (clc.netchan.alternateProtocol == 0 ? + PROTOCOL_VERSION : clc.netchan.alternateProtocol == 1 ? 70 : 69)); + } + else + { + int number; + + // scan for a free demo name + for (number = 0; number <= 9999; number++) + { + CL_DemoFilename(number, demoName, sizeof(demoName)); + Com_sprintf(name, sizeof(name), "demos/%s.%s%d", demoName, DEMOEXT, PROTOCOL_VERSION); + + if (!FS_FileExists(name)) break; // file doesn't exist + } + } + + // open the demo file + + Com_Printf("recording to %s.\n", name); + clc.demofile = FS_FOpenFileWrite(name); + if (!clc.demofile) + { + Com_Printf("ERROR: couldn't open.\n"); + return; + } + clc.demorecording = true; + if (Cvar_VariableValue("ui_recordSPDemo")) + { + clc.spDemoRecording = true; + } + else + { + clc.spDemoRecording = false; + } + + Q_strncpyz(clc.demoName, demoName, sizeof(clc.demoName)); + + // don't start saving messages until a non-delta compressed message is received + clc.demowaiting = true; + + // write out the gamestate message + MSG_Init(&buf, bufData, sizeof(bufData)); + MSG_Bitstream(&buf); + + // NOTE, MRE: all server->client messages now acknowledge + MSG_WriteLong(&buf, clc.reliableSequence); + + MSG_WriteByte(&buf, svc_gamestate); + MSG_WriteLong(&buf, clc.serverCommandSequence); + + // configstrings + for (i = 0; i < MAX_CONFIGSTRINGS; i++) + { + if (!cl.gameState.stringOffsets[i]) + { + continue; + } + const char *s = cl.gameState.stringData + cl.gameState.stringOffsets[i]; + MSG_WriteByte(&buf, svc_configstring); + MSG_WriteShort(&buf, i); + MSG_WriteBigString(&buf, s); + } + + // baselines + ::memset(&nullstate, 0, sizeof(nullstate)); + for (i = 0; i < MAX_GENTITIES; i++) + { + ent = &cl.entityBaselines[i]; + if (!ent->number) + { + continue; + } + MSG_WriteByte(&buf, svc_baseline); + MSG_WriteDeltaEntity(clc.netchan.alternateProtocol, &buf, &nullstate, ent, true); + } + + MSG_WriteByte(&buf, svc_EOF); + + // finished writing the gamestate stuff + + // write the client num + MSG_WriteLong(&buf, clc.clientNum); + // write the checksum feed + MSG_WriteLong(&buf, clc.checksumFeed); + + // finished writing the client packet + MSG_WriteByte(&buf, svc_EOF); + + // write it to the demo file + len = LittleLong(clc.serverMessageSequence - 1); + FS_Write(&len, 4, clc.demofile); + + len = LittleLong(buf.cursize); + FS_Write(&len, 4, clc.demofile); + FS_Write(buf.data, buf.cursize, clc.demofile); + + // the rest of the demo file will be copied from net messages +} + +/* +======================================================================= + +CLIENT SIDE DEMO PLAYBACK + +======================================================================= +*/ + +/* +================= +CL_DemoFrameDurationSDev +================= +*/ +static float CL_DemoFrameDurationSDev(void) +{ + int i; + int numFrames; + float mean = 0.0f; + float variance = 0.0f; + + if ((clc.timeDemoFrames - 1) > MAX_TIMEDEMO_DURATIONS) + numFrames = MAX_TIMEDEMO_DURATIONS; + else + numFrames = clc.timeDemoFrames - 1; + + for (i = 0; i < numFrames; i++) mean += clc.timeDemoDurations[i]; + mean /= numFrames; + + for (i = 0; i < numFrames; i++) + { + float x = clc.timeDemoDurations[i]; + + variance += ((x - mean) * (x - mean)); + } + variance /= numFrames; + + return sqrt(variance); +} + +/* +================= +CL_DemoCompleted +================= +*/ +void CL_DemoCompleted(void) +{ + char buffer[MAX_STRING_CHARS]; + + if (cl_timedemo && cl_timedemo->integer) + { + int time; + + time = Sys_Milliseconds() - clc.timeDemoStart; + if (time > 0) + { + // Millisecond times are frame durations: + // minimum/average/maximum/std deviation + Com_sprintf(buffer, sizeof(buffer), "%i frames %3.1f seconds %3.1f fps %d.0/%.1f/%d.0/%.1f ms\n", + clc.timeDemoFrames, time / 1000.0, clc.timeDemoFrames * 1000.0 / time, clc.timeDemoMinDuration, + time / (float)clc.timeDemoFrames, clc.timeDemoMaxDuration, CL_DemoFrameDurationSDev()); + Com_Printf("%s", buffer); + + // Write a log of all the frame durations + if (cl_timedemoLog && strlen(cl_timedemoLog->string) > 0) + { + int i; + int numFrames; + fileHandle_t f; + + if ((clc.timeDemoFrames - 1) > MAX_TIMEDEMO_DURATIONS) + numFrames = MAX_TIMEDEMO_DURATIONS; + else + numFrames = clc.timeDemoFrames - 1; + + f = FS_FOpenFileWrite(cl_timedemoLog->string); + if (f) + { + FS_Printf(f, "# %s", buffer); + + for (i = 0; i < numFrames; i++) FS_Printf(f, "%d\n", clc.timeDemoDurations[i]); + + FS_FCloseFile(f); + Com_Printf("%s written\n", cl_timedemoLog->string); + } + else + { + Com_Printf("Couldn't open %s for writing\n", cl_timedemoLog->string); + } + } + } + } + + CL_Disconnect(true); + CL_NextDemo(); +} + +/* +================= +CL_ReadDemoMessage +================= +*/ +void CL_ReadDemoMessage(void) +{ + int r; + msg_t buf; + byte bufData[MAX_MSGLEN]; + int s; + + if (!clc.demofile) + { + CL_DemoCompleted(); + return; + } + + // get the sequence number + r = FS_Read(&s, 4, clc.demofile); + if (r != 4) + { + CL_DemoCompleted(); + return; + } + clc.serverMessageSequence = LittleLong(s); + + // init the message + MSG_Init(&buf, bufData, sizeof(bufData)); + + // get the length + r = FS_Read(&buf.cursize, 4, clc.demofile); + if (r != 4) + { + CL_DemoCompleted(); + return; + } + buf.cursize = LittleLong(buf.cursize); + if (buf.cursize == -1) + { + CL_DemoCompleted(); + return; + } + if (buf.cursize > buf.maxsize) + { + Com_Error(ERR_DROP, "CL_ReadDemoMessage: demoMsglen > MAX_MSGLEN"); + } + r = FS_Read(buf.data, buf.cursize, clc.demofile); + if (r != buf.cursize) + { + Com_Printf("Demo file was truncated.\n"); + CL_DemoCompleted(); + return; + } + + clc.lastPacketTime = cls.realtime; + buf.readcount = 0; + CL_ParseServerMessage(&buf); +} + +/* +==================== +CL_WalkDemoExt +==================== +*/ +static int CL_WalkDemoExt(const char *arg, char *name, int *demofile) +{ + int i; + *demofile = 0; + + Com_sprintf(name, MAX_OSPATH, "demos/%s.%s%d", arg, DEMOEXT, PROTOCOL_VERSION); + FS_FOpenFileRead(name, demofile, true); + + if (*demofile) + { + Com_Printf("Demo file: %s\n", name); + return PROTOCOL_VERSION; + } + + Com_Printf("Not found: %s\n", name); + + for (i = 0; demo_protocols[i]; ++i) + { + if (demo_protocols[i] == PROTOCOL_VERSION) continue; + + Com_sprintf(name, MAX_OSPATH, "demos/%s.%s%d", arg, DEMOEXT, demo_protocols[i]); + FS_FOpenFileRead(name, demofile, true); + if (*demofile) + { + Com_Printf("Demo file: %s\n", name); + + return demo_protocols[i]; + } + else + Com_Printf("Not found: %s\n", name); + } + + return -1; +} + +/* +==================== +CL_CompleteDemoName +==================== +*/ +static void CL_CompleteDemoName(char *args, int argNum) +{ + if (argNum == 2) + { + char demoExt[16]; + + Com_sprintf(demoExt, sizeof(demoExt), ".%s%d", DEMOEXT, PROTOCOL_VERSION); + Field_CompleteFilename("demos", demoExt, true, true); + } +} + +/* +==================== +CL_PlayDemo_f + +demo <demoname> + +==================== +*/ +void CL_PlayDemo_f(void) +{ + char name[MAX_OSPATH]; + const char *ext_test; + int protocol, i; + char retry[MAX_OSPATH]; + + if (Cmd_Argc() != 2) + { + Com_Printf("demo <demoname>\n"); + return; + } + + // make sure a local server is killed + // 2 means don't force disconnect of local client + Cvar_Set("sv_killserver", "2"); + + // open the demo file + const char *arg = Cmd_Argv(1); + + CL_Disconnect(true); + + // check for an extension .DEMOEXT_?? (?? is protocol) + ext_test = strrchr(arg, '.'); + + if (ext_test && !Q_stricmpn(ext_test + 1, DEMOEXT, ARRAY_LEN(DEMOEXT) - 1)) + { + protocol = atoi(ext_test + ARRAY_LEN(DEMOEXT)); + + for (i = 0; demo_protocols[i]; i++) + { + if (demo_protocols[i] == protocol) break; + } + + if (demo_protocols[i] || protocol == PROTOCOL_VERSION) + { + Com_sprintf(name, sizeof(name), "demos/%s", arg); + FS_FOpenFileRead(name, &clc.demofile, true); + } + else + { + int len; + + Com_Printf("Protocol %d not supported for demos\n", protocol); + len = ext_test - arg; + + if (len >= ARRAY_LEN(retry)) len = ARRAY_LEN(retry) - 1; + + Q_strncpyz(retry, arg, len + 1); + retry[len] = '\0'; + protocol = CL_WalkDemoExt(retry, name, &clc.demofile); + } + } + else + protocol = CL_WalkDemoExt(arg, name, &clc.demofile); + + if (!clc.demofile) + { + Com_Error(ERR_DROP, "couldn't open %s", name); + return; + } + Q_strncpyz(clc.demoName, arg, sizeof(clc.demoName)); + + clc.state = CA_CONNECTED; + clc.demoplaying = true; + Q_strncpyz(clc.servername, arg, sizeof(clc.servername)); + clc.netchan.alternateProtocol = (protocol == 69 ? 2 : protocol == 70 ? 1 : 0); + + // read demo messages until connected + while (clc.state >= CA_CONNECTED && clc.state < CA_PRIMED) + { + CL_ReadDemoMessage(); + } + // don't get the first snapshot this frame, to prevent the long + // time from the gamestate load from messing causing a time skip + clc.firstDemoFrameSkipped = false; +} + +/* +==================== +CL_StartDemoLoop + +Closing the main menu will restart the demo loop +==================== +*/ +static void CL_StartDemoLoop(void) +{ + // start the demo loop again + Cbuf_AddText("d1\n"); + Key_SetCatcher(Key_GetCatcher() & KEYCATCH_CONSOLE); +} + +/* +================== +CL_NextDemo + +Called when a demo or cinematic finishes +If the "nextdemo" cvar is set, that command will be issued +================== +*/ +void CL_NextDemo(void) +{ + char v[MAX_STRING_CHARS]; + + Q_strncpyz(v, Cvar_VariableString("nextdemo"), sizeof(v)); + v[MAX_STRING_CHARS - 1] = 0; + Com_DPrintf("CL_NextDemo: %s\n", v); + if (!v[0]) + { + return; + } + + Cvar_Set("nextdemo", ""); + Cbuf_AddText(v); + Cbuf_AddText("\n"); + Cbuf_Execute(); +} + +/* +================== +CL_DemoState + +Returns the current state of the demo system +================== +*/ +demoState_t CL_DemoState(void) +{ + if (clc.demoplaying) + { + return DS_PLAYBACK; + } + else if (clc.demorecording) + { + return DS_RECORDING; + } + else + { + return DS_NONE; + } +} + +/* +================== +CL_DemoPos + +Returns the current position of the demo +================== +*/ +int CL_DemoPos(void) +{ + if (clc.demoplaying || clc.demorecording) + { + return FS_FTell(clc.demofile); + } + else + { + return 0; + } +} + +/* +================== +CL_DemoName + +Returns the name of the demo +================== +*/ +void CL_DemoName(char *buffer, int size) +{ + if (clc.demoplaying || clc.demorecording) + { + Q_strncpyz(buffer, clc.demoName, size); + } + else if (size >= 1) + { + buffer[0] = '\0'; + } +} + +//====================================================================== + +/* +===================== +CL_ClearState + +Called before parsing a gamestate +===================== +*/ +void CL_ClearState(void) +{ + // S_StopAllSounds(); + + ::memset(&cl, 0, sizeof(cl)); +} + +/* +==================== +CL_UpdateGUID + +update cl_guid using QKEY_FILE and optional prefix +==================== +*/ +static void CL_UpdateGUID(const char *prefix, int prefix_len) +{ + fileHandle_t f; + int len; + + len = FS_SV_FOpenFileRead(QKEY_FILE, &f); + FS_FCloseFile(f); + + if (len != QKEY_SIZE) + Cvar_Set("cl_guid", ""); + else + Cvar_Set("cl_guid", Com_MD5File(QKEY_FILE, QKEY_SIZE, prefix, prefix_len)); +} + +static void CL_OldGame(void) +{ + if (cl_oldGameSet) + { + // change back to previous fs_game + cl_oldGameSet = false; + Cvar_Set2("fs_game", cl_oldGame, true); + FS_ConditionalRestart(clc.checksumFeed, false); + } +} + +/* +===================== +CL_Disconnect + +Called when a connection, demo, or cinematic is being terminated. +Goes from a connected state to either a menu state or a console state +Sends a disconnect message to the server +This is also called on Com_Error and Com_Quit, so it shouldn't cause any errors +===================== +*/ +void CL_Disconnect(bool showMainMenu) +{ + if (!com_cl_running || !com_cl_running->integer) + { + return; + } + + // shutting down the client so enter full screen ui mode + Cvar_Set("r_uiFullScreen", "1"); + + if (clc.demorecording) + { + CL_StopRecord_f(); + } + + if (clc.download) + { + FS_FCloseFile(clc.download); + clc.download = 0; + } + *clc.downloadTempName = *clc.downloadName = 0; + Cvar_Set("cl_downloadName", ""); + +#ifdef USE_MUMBLE + if (cl_useMumble->integer && mumble_islinked()) + { + Com_Printf("Mumble: Unlinking from Mumble application\n"); + mumble_unlink(); + } +#endif + +#ifdef USE_VOIP + if (cl_voipSend->integer) + { + int tmp = cl_voipUseVAD->integer; + cl_voipUseVAD->integer = 0; // disable this for a moment. + clc.voipOutgoingDataSize = 0; // dump any pending VoIP transmission. + Cvar_Set("cl_voipSend", "0"); + CL_CaptureVoip(); // clean up any state... + cl_voipUseVAD->integer = tmp; + } + + if (clc.voipCodecInitialized) + { + int i; + opus_encoder_destroy(clc.opusEncoder); + for (i = 0; i < MAX_CLIENTS; i++) + { + opus_decoder_destroy(clc.opusDecoder[i]); + } + } + Cmd_RemoveCommand("voip"); +#endif + + if (clc.demofile) + { + FS_FCloseFile(clc.demofile); + clc.demofile = 0; + } + + if (cls.ui && showMainMenu) + { + VM_Call(cls.ui, UI_SET_ACTIVE_MENU - (cls.uiInterface == 2 ? 2 : 0), UIMENU_NONE); + } + + SCR_StopCinematic(); + S_ClearSoundBuffer(); + + // send a disconnect message to the server + // send it a few times in case one is dropped + if (clc.state >= CA_CONNECTED) + { + CL_AddReliableCommand("disconnect", true); + CL_WritePacket(); + CL_WritePacket(); + CL_WritePacket(); + } + + // Remove pure paks + FS_PureServerSetLoadedPaks("", ""); + FS_PureServerSetReferencedPaks("", ""); + + CL_ClearState(); + + // wipe the client connection + ::memset(&clc, 0, sizeof(clc)); + + clc.state = CA_DISCONNECTED; + + CL_ProtocolSpecificCommandsInit(); + + // allow cheats locally + Cvar_Set("sv_cheats", "1"); + + // not connected to a pure server anymore + cl_connectedToPureServer = false; + +#ifdef USE_VOIP + // not connected to voip server anymore. + clc.voipEnabled = false; +#endif + + // Stop recording any video + if (CL_VideoRecording()) + { + // Finish rendering current frame + SCR_UpdateScreen(); + CL_CloseAVI(); + } + + CL_UpdateGUID(NULL, 0); + + if (!noGameRestart) + CL_OldGame(); + else + noGameRestart = false; +} + +/* +=================== +CL_ForwardCommandToServer + +adds the current command line as a clientCommand +things like godmode, noclip, etc, are commands directed to the server, +so when they are typed in at the console, they will need to be forwarded. +=================== +*/ +void CL_ForwardCommandToServer(const char *string) +{ + const char *cmd = Cmd_Argv(0); + + // ignore key up commands + if (cmd[0] == '-') + { + return; + } + + if (clc.demoplaying || clc.state < CA_CONNECTED || cmd[0] == '+') + { + Com_Printf("Unknown command \"%s" S_COLOR_WHITE "\"\n", cmd); + return; + } + + if (Cmd_Argc() > 1) + { + CL_AddReliableCommand(string, false); + } + else + { + CL_AddReliableCommand(cmd, false); + } +} + +/* +=================== +CL_RequestMotd + +=================== +*/ +static void CL_RequestMotd(void) +{ + char info[MAX_INFO_STRING]; + + if (!cl_motd->integer) + { + return; + } + Com_Printf("Resolving %s\n", MASTER_SERVER_NAME); + + switch (NET_StringToAdr(MASTER_SERVER_NAME, &cls.updateServer, NA_UNSPEC)) + { + case 0: + Com_Printf("Couldn't resolve master address\n"); + return; + + case 2: + cls.updateServer.port = BigShort(PORT_MASTER); + default: + break; + } + + Com_Printf("%s resolved to %s\n", MASTER_SERVER_NAME, NET_AdrToStringwPort(cls.updateServer)); + + info[0] = 0; + + Com_sprintf(cls.updateChallenge, sizeof(cls.updateChallenge), "%i", ((rand() << 16) ^ rand()) ^ Com_Milliseconds()); + + Info_SetValueForKey(info, "challenge", cls.updateChallenge); + + NET_OutOfBandPrint(NS_CLIENT, cls.updateServer, "getmotd%s", info); +} + +/* +====================================================================== + +CONSOLE COMMANDS + +====================================================================== +*/ + +/* +================== +CL_ShowIP_f +================== +*/ +static void CL_ShowIP_f(void) { Sys_ShowIP(); } + +/* +================== +CL_ForwardToServer_f +================== +*/ +static void CL_ForwardToServer_f(void) +{ + if (clc.state != CA_ACTIVE || clc.demoplaying) + { + Com_Printf("Not connected to a server.\n"); + return; + } + + // don't forward the first argument + if (Cmd_Argc() > 1) + { + CL_AddReliableCommand(Cmd_Args(), false); + } +} + +/* +================== +CL_Disconnect_f +================== +*/ +void CL_Disconnect_f(void) +{ + SCR_StopCinematic(); + if (clc.state != CA_DISCONNECTED && clc.state != CA_CINEMATIC) + { + Com_Error(ERR_DISCONNECT, "Disconnected from server"); + } +} + +/* +================ +CL_Reconnect_f + +================ +*/ +static void CL_Reconnect_f(void) +{ + if (!strlen(cl_reconnectArgs)) return; + Cbuf_AddText(va("connect %s\n", cl_reconnectArgs)); +} + +/* +================ +CL_Connect_f + +================ +*/ +void CL_Connect_f(void) +{ + const char *server; + int alternateProtocol; + const char *serverString; + int argc = Cmd_Argc(); + netadrtype_t family = NA_UNSPEC; + + if (argc < 2 || argc > 4) + { + Com_Printf("usage: connect [-4|-6] server [-g|-1]\n"); + return; + } + + alternateProtocol = 0; + if (argc == 2) + { + } + else if (!strcmp(Cmd_Argv(argc - 1), "-g")) + { + alternateProtocol = 1; + --argc; + } + else if (!strcmp(Cmd_Argv(argc - 1), "-1")) + { + alternateProtocol = 2; + --argc; + } + else if (argc == 4) + { + --argc; + } + + if (argc == 2) + server = Cmd_Argv(1); + else + { + if (!strcmp(Cmd_Argv(1), "-4")) + family = NA_IP; + else if (!strcmp(Cmd_Argv(1), "-6")) + family = NA_IP6; + else + Com_Printf("warning: only -4 or -6 as address type understood.\n"); + + server = Cmd_Argv(2); + } + + // save arguments for reconnect + Q_strncpyz(cl_reconnectArgs, Cmd_Args(), sizeof(cl_reconnectArgs)); + + // clear any previous "server full" type messages + clc.serverMessage[0] = 0; + + if (com_sv_running->integer && !strcmp(server, "localhost")) + { + // if running a local server, kill it + SV_Shutdown("Server quit"); + } + + // make sure a local server is killed + Cvar_Set("sv_killserver", "1"); + SV_Frame(0); + + noGameRestart = true; + CL_Disconnect(true); + + Q_strncpyz(clc.servername, server, sizeof(clc.servername)); + + if (!NET_StringToAdr(clc.servername, &clc.serverAddress, family)) + { + Com_Printf("Bad server address\n"); + clc.state = CA_DISCONNECTED; + CL_ProtocolSpecificCommandsInit(); + return; + } + if (clc.serverAddress.port == 0) + { + clc.serverAddress.port = BigShort(PORT_SERVER); + } + clc.serverAddress.alternateProtocol = alternateProtocol; + + serverString = NET_AdrToStringwPort(clc.serverAddress); + + Com_Printf("%s resolved to %s\n", clc.servername, serverString); + + if (cl_guidServerUniq->integer) + CL_UpdateGUID(serverString, strlen(serverString)); + else + CL_UpdateGUID(NULL, 0); + + clc.challenge2[0] = '\0'; + clc.sendSignature = false; + + // if we aren't playing on a lan, we need to authenticate + // with the cd key + if (NET_IsLocalAddress(clc.serverAddress)) + { + clc.state = CA_CHALLENGING; + clc.sendSignature = true; + } + else + { + clc.state = CA_CONNECTING; + + // Set a client challenge number that ideally is mirrored back by the server. + clc.challenge = ((rand() << 16) ^ rand()) ^ Com_Milliseconds(); + } + + Key_SetCatcher(Key_GetCatcher() & KEYCATCH_CONSOLE); + clc.connectTime = -99999; // CL_CheckForResend() will fire immediately + clc.connectPacketCount = 0; +} + +#define MAX_RCON_MESSAGE 1024 + +/* +================== +CL_CompleteRcon +================== +*/ +static void CL_CompleteRcon(char *args, int argNum) +{ + if (argNum == 2) + { + // Skip "rcon " + char *p = Com_SkipTokens(args, 1, " "); + + if (p > args) Field_CompleteCommand(p, true, true); + } +} + +/* +===================== +CL_Rcon_f + + Send the rest of the command line over as + an unconnected command. +===================== +*/ +static void CL_Rcon_f(void) +{ + char message[MAX_RCON_MESSAGE]; + netadr_t to; + + if (!rcon_client_password->string[0]) + { + Com_Printf( + "You must set 'rconpassword' before\n" + "issuing an rcon command.\n"); + return; + } + + message[0] = -1; + message[1] = -1; + message[2] = -1; + message[3] = -1; + message[4] = 0; + + Q_strcat(message, MAX_RCON_MESSAGE, "rcon "); + + Q_strcat(message, MAX_RCON_MESSAGE, rcon_client_password->string); + Q_strcat(message, MAX_RCON_MESSAGE, " "); + + // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=543 + Q_strcat(message, MAX_RCON_MESSAGE, Cmd_Cmd() + 5); + + if (clc.state >= CA_CONNECTED) + { + to = clc.netchan.remoteAddress; + } + else + { + if (!strlen(rconAddress->string)) + { + Com_Printf( + "You must either be connected,\n" + "or set the 'rconAddress' cvar\n" + "to issue rcon commands\n"); + + return; + } + NET_StringToAdr(rconAddress->string, &to, NA_UNSPEC); + if (to.port == 0) + { + to.port = BigShort(PORT_SERVER); + } + } + + NET_SendPacket(NS_CLIENT, strlen(message) + 1, message, to); +} + +/* +================= +CL_SendPureChecksums +================= +*/ +static void CL_SendPureChecksums(void) +{ + char cMsg[MAX_INFO_VALUE]; + + // if we are pure we need to send back a command with our referenced pk3 checksums + Com_sprintf(cMsg, sizeof(cMsg), "cp %d %s", cl.serverId, FS_ReferencedPakPureChecksums()); + + CL_AddReliableCommand(cMsg, false); +} + +/* +================= +CL_ResetPureClientAtServer +================= +*/ +static void CL_ResetPureClientAtServer(void) { CL_AddReliableCommand("vdr", false); } + +/* +================= +CL_Snd_Restart + +Restart the sound subsystem +================= +*/ +static void CL_Snd_Shutdown(void) +{ + S_Shutdown(); + cls.soundStarted = false; +} +/* +================== +CL_PK3List_f +================== +*/ +static void CL_OpenedPK3List_f(void) { Com_Printf("Opened PK3 Names: %s\n", FS_LoadedPakNames(false)); } + +/* +================== +CL_PureList_f +================== +*/ +static void CL_ReferencedPK3List_f(void) { Com_Printf("Referenced PK3 Names: %s\n", FS_ReferencedPakNames(false)); } + +/* +================== +CL_Configstrings_f +================== +*/ +static void CL_Configstrings_f(void) +{ + int i; + int ofs; + + if (clc.state != CA_ACTIVE) + { + Com_Printf("Not connected to a server.\n"); + return; + } + + for (i = 0; i < MAX_CONFIGSTRINGS; i++) + { + ofs = cl.gameState.stringOffsets[i]; + if (!ofs) + { + continue; + } + Com_Printf("%4i: %s\n", i, cl.gameState.stringData + ofs); + } +} + +/* +============== +CL_Clientinfo_f +============== +*/ +static void CL_Clientinfo_f(void) +{ + Com_Printf("--------- Client Information ---------\n"); + Com_Printf("state: %i\n", clc.state); + Com_Printf("Server: %s\n", clc.servername); + Com_Printf("User info settings:\n"); + Info_Print(Cvar_InfoString(CVAR_USERINFO)); + Com_Printf("--------------------------------------\n"); +} + +//==================================================================== + +/* +================= +CL_DownloadsComplete + +Called when all downloading has been completed +================= +*/ +static void CL_DownloadsComplete(void) +{ + Com_Printf("Downloads complete\n"); + + // if we downloaded with cURL + if (clc.cURLUsed) + { + clc.cURLUsed = false; + CL_cURL_Shutdown(); + if (clc.cURLDisconnected) + { + if (clc.downloadRestart) + { + if (!clc.activeCURLNotGameRelated) FS_Restart(clc.checksumFeed); + clc.downloadRestart = false; + } + clc.cURLDisconnected = false; + if (!clc.activeCURLNotGameRelated) CL_Reconnect_f(); + return; + } + } + + // if we downloaded files we need to restart the file system + if (clc.downloadRestart) + { + clc.downloadRestart = false; + + FS_Restart(clc.checksumFeed); // We possibly downloaded a pak, restart the file system to load it + + // inform the server so we get new gamestate info + CL_AddReliableCommand("donedl", false); + + // by sending the donedl command we request a new gamestate + // so we don't want to load stuff yet + return; + } + else + { + FS_ClearPakReferences(0); + } + + // let the client game init and load data + clc.state = CA_LOADING; + + // Pump the loop, this may change gamestate! + Com_EventLoop(); + + // if the gamestate was changed by calling Com_EventLoop + // then we loaded everything already and we don't want to do it again. + if (clc.state != CA_LOADING) + { + return; + } + + // starting to load a map so we get out of full screen ui mode + Cvar_Set("r_uiFullScreen", "0"); + + // flush client memory and start loading stuff + // this will also (re)load the UI + // if this is a local client then only the client part of the hunk + // will be cleared, note that this is done after the hunk mark has been set + CL_FlushMemory(); + + // initialize the CGame + cls.cgameStarted = true; + CL_InitCGame(); + + // set pure checksums + CL_SendPureChecksums(); + + CL_WritePacket(); + CL_WritePacket(); + CL_WritePacket(); +} + +/* +================= +CL_BeginDownload + +Requests a file to download from the server. Stores it in the current +game directory. +================= +*/ +static void CL_BeginDownload(const char *localName, const char *remoteName) +{ + Com_DPrintf( + "***** CL_BeginDownload *****\n" + "Localname: %s\n" + "Remotename: %s\n" + "****************************\n", + localName, remoteName); + + Q_strncpyz(clc.downloadName, localName, sizeof(clc.downloadName)); + Com_sprintf(clc.downloadTempName, sizeof(clc.downloadTempName), "%s.tmp", localName); + + // Set so UI gets access to it + Cvar_Set("cl_downloadName", remoteName); + Cvar_Set("cl_downloadSize", "0"); + Cvar_Set("cl_downloadCount", "0"); + Cvar_SetValue("cl_downloadTime", cls.realtime); + + clc.downloadBlock = 0; // Starting new file + clc.downloadCount = 0; + + // Stop any errant looping sounds that may be playing + S_ClearLoopingSounds(true); + + CL_AddReliableCommand(va("download %s", remoteName), false); +} + +/* +================= +CL_NextDownload + +A download completed or failed +================= +*/ +void CL_NextDownload(void) +{ + char *s; + char *remoteName, *localName; + bool useCURL = false; + int prompt; + + // A download has finished, check whether this matches a referenced checksum + if (*clc.downloadName && !clc.activeCURLNotGameRelated) + { + char *zippath = FS_BuildOSPath(Cvar_VariableString("fs_homepath"), clc.downloadName, ""); + zippath[strlen(zippath) - 1] = '\0'; + + if (!FS_CompareZipChecksum(zippath)) Com_Error(ERR_DROP, "Incorrect checksum for file: %s", clc.downloadName); + } + + *clc.downloadTempName = *clc.downloadName = 0; + Cvar_Set("cl_downloadName", ""); + + // We are looking to start a download here + if (*clc.downloadList) + { + // Prompt if we do not allow automatic downloads + prompt = com_downloadPrompt->integer; + if (!(prompt & DLP_TYPE_MASK) && !(cl_allowDownload->integer & DLF_ENABLE)) + { + char files[MAX_INFO_STRING] = ""; + char *name, *head; + const char *pure_msg; + const char *url_msg = ""; + int i = 0, others = 0, swap = 0, max_list = 12; + + // Set the download URL message + if ((clc.sv_allowDownload & DLF_ENABLE) && !(clc.sv_allowDownload & DLF_NO_REDIRECT)) + { + url_msg = va("The server redirects to the following URL:\n%s", clc.sv_dlURL); + max_list -= 6; + } + + // Make a pretty version of the download list + name = clc.downloadList; + if (*name == '@') name++; + + do + { + // Copy remote name + head = name; + while (*head && *head != '@') head++; + + swap = *head; + *head = 0; + + if (i++ < max_list) + { + if (i > 1) Q_strcat(files, sizeof(files), ", "); + Q_strcat(files, sizeof(files), name); + } + else + { + others++; + } + + *head = swap; + if (!swap) break; + + // Skip local name + head++; + while (*head && *head != '@') head++; + + name = head + 1; + } while (*head); + + if (others) + { + Q_strcat(files, sizeof(files), va("(%d other file%s)\n", others, others > 1 ? "s" : "")); + } + + // Set the pure message + if (cl_connectedToPureServer) + { + if (!(clc.sv_allowDownload & DLF_ENABLE) || + ((clc.sv_allowDownload & DLF_NO_UDP) && (clc.sv_allowDownload & DLF_NO_REDIRECT))) + { + pure_msg = + "You are missing files required by the server. " + "The server does not allow downloading. " + "You must install these files manually:"; + } + else + { + pure_msg = + "You are missing files required by the server. " + "You must download these files or disconnect:"; + } + } + else + { + pure_msg = + "You are missing optional files provided by the " + "server. You may not need them to play but can " + "choose to download them anyway:"; + } + + Cvar_Set("com_downloadPromptText", va("%s\n\n%s\n%s", pure_msg, files, url_msg)); + Cvar_Set("com_downloadPrompt", va("%d", DLP_SHOW)); + return; + } + + if (!(prompt & DLP_PROMPTED)) Cvar_Set("com_downloadPrompt", va("%d", prompt | DLP_PROMPTED)); + + prompt &= DLP_TYPE_MASK; + + s = clc.downloadList; + + // format is: + // @remotename@localname@remotename@localname, etc. + + if (*s == '@') s++; + remoteName = s; + + if ((s = strchr(s, '@')) == NULL) + { + CL_DownloadsComplete(); + return; + } + + *s++ = 0; + localName = s; + if ((s = strchr(s, '@')) != NULL) + *s++ = 0; + else + s = localName + strlen(localName); // point at the nul byte + + if (((cl_allowDownload->integer & DLF_ENABLE) && !(cl_allowDownload->integer & DLF_NO_REDIRECT)) || + prompt == DLP_CURL) + { + Com_Printf("Trying CURL download: %s; %s\n", localName, remoteName); + if (clc.sv_allowDownload & DLF_NO_REDIRECT) + { + Com_Printf( + "WARNING: server does not " + "allow download redirection " + "(sv_allowDownload is %d)\n", + clc.sv_allowDownload); + } + else if (!*clc.sv_dlURL) + { + Com_Printf( + "WARNING: server allows " + "download redirection, but does not " + "have sv_dlURL set\n"); + } + else if (!CL_cURL_Init()) + { + Com_Printf( + "WARNING: could not load " + "cURL library\n"); + } + else + { + CL_cURL_BeginDownload(localName, va("%s/%s", clc.sv_dlURL, remoteName)); + useCURL = true; + } + } + else if (!(clc.sv_allowDownload & DLF_NO_REDIRECT)) + { + Com_Printf( + "WARNING: server allows download " + "redirection, but it disabled by client " + "configuration (cl_allowDownload is %d)\n", + cl_allowDownload->integer); + } + if (!useCURL) + { + Com_Printf("Trying UDP download: %s; %s\n", localName, remoteName); + + if ((!(cl_allowDownload->integer & DLF_ENABLE) || (cl_allowDownload->integer & DLF_NO_UDP)) && + prompt != DLP_UDP) + { + if (cl_connectedToPureServer) + { + Com_Error(ERR_DROP, + "Automatic downloads are " + "disabled on your client (cl_allowDownload is %d). " + "You can enable automatic downloads in the Options " + "menu.", + cl_allowDownload->integer); + return; + } + + Com_Printf("WARNING: UDP downloads are disabled.\n"); + CL_DownloadsComplete(); + return; + } + else + { + CL_BeginDownload(localName, remoteName); + } + } + clc.downloadRestart = true; + + // move over the rest + memmove(clc.downloadList, s, strlen(s) + 1); + + return; + } + + CL_DownloadsComplete(); +} + +/* +================= +CL_InitDownloads + +After receiving a valid game state, we valid the cgame and local zip files here +and determine if we need to download them +================= +*/ +void CL_InitDownloads(void) +{ + if (FS_ComparePaks(clc.downloadList, sizeof(clc.downloadList), true)) + { + Com_Printf("Need paks: %s\n", clc.downloadList); + + Cvar_Set("com_downloadPrompt", "0"); + if (*clc.downloadList) + { + // if autodownloading is not enabled on the server + clc.state = CA_CONNECTED; + *clc.downloadTempName = *clc.downloadName = 0; + Cvar_Set("cl_downloadName", ""); + CL_NextDownload(); + return; + } + } + CL_DownloadsComplete(); +} + +/* +=============== +CL_UnloadRSAKeypair +=============== +*/ +static void CL_UnloadRSAKeypair(void) +{ + rsa_public_key_clear(&cls.rsa.public_key); + rsa_private_key_clear(&cls.rsa.private_key); +} + +/* +=============== +CL_WriteRSAPublicKey +=============== +*/ +static bool CL_WriteRSAPublicKey(void) +{ + struct nettle_buffer key_buffer; + fileHandle_t f; + + f = FS_SV_FOpenFileWrite(RSA_PUBLIC_KEY_FILE); + if (!f) return false; + + nettle_buffer_init(&key_buffer); + if (!rsa_keypair_to_sexp(&key_buffer, NULL, &cls.rsa.public_key, NULL)) + { + FS_FCloseFile(f); + nettle_buffer_clear(&key_buffer); + return false; + } + + FS_Write(key_buffer.contents, key_buffer.size, f); + FS_FCloseFile(f); + + nettle_buffer_clear(&key_buffer); + return true; +} + +/* +=============== +CL_WriteRSAPrivateKey +=============== +*/ +static bool CL_WriteRSAPrivateKey(void) +{ + struct nettle_buffer key_buffer; + fileHandle_t f; + +#ifndef _WIN32 + int old_umask = umask(0377); +#endif + f = FS_SV_FOpenFileWrite(RSA_PRIVATE_KEY_FILE); +#ifndef _WIN32 + umask(old_umask); +#endif + if (!f) return false; + + nettle_buffer_init(&key_buffer); + if (!rsa_keypair_to_sexp(&key_buffer, NULL, &cls.rsa.public_key, &cls.rsa.private_key)) + { + FS_FCloseFile(f); + nettle_buffer_clear(&key_buffer); + return false; + } + + FS_Write(key_buffer.contents, key_buffer.size, f); + FS_FCloseFile(f); + + nettle_buffer_clear(&key_buffer); + return true; +} + +/* +=============== +CL_GenerateRSAKeypair + +public_key and private_key must already be inititalized before calling this +function. This is done by CL_LoadRSAKeypair. +=============== +*/ +static void CL_GenerateRSAKeypair(void) +{ + mpz_set_ui(cls.rsa.public_key.e, RSA_PUBLIC_EXPONENT); + + int success = rsa_generate_keypair( + &cls.rsa.public_key, &cls.rsa.private_key, NULL, qnettle_random, NULL, NULL, RSA_KEY_LENGTH, 0); + if (success) + if (CL_WriteRSAPrivateKey()) + if (CL_WriteRSAPublicKey()) + { + Com_Printf("RSA keypair generated\n"); + return; + } + + // failure + CL_UnloadRSAKeypair(); + Com_Printf("Error generating RSA keypair, setting cl_rsaAuth to 0\n"); + Cvar_Set("cl_rsaAuth", "0"); +} + +/* +=============== +CL_LoadRSAKeypair + +Attempt to load RSA keys from RSA_PRIVATE_KEY_FILE +If this fails, generate a new keypair +=============== +*/ +static void CL_LoadRSAKeypair(void) +{ + int len; + fileHandle_t f; + uint8_t *buf; + + rsa_public_key_init(&cls.rsa.public_key); + rsa_private_key_init(&cls.rsa.private_key); + + Com_DPrintf("Loading RSA private key from %s\n", RSA_PRIVATE_KEY_FILE); + + len = FS_SV_FOpenFileRead(RSA_PRIVATE_KEY_FILE, &f); + if (!f) + { + Com_DPrintf("RSA private key not found, generating\n"); + CL_GenerateRSAKeypair(); + return; + } + + if (len < 1) + { + Com_DPrintf("RSA private key empty, generating\n"); + FS_FCloseFile(f); + CL_GenerateRSAKeypair(); + return; + } + + buf = (uint8_t *)Z_Malloc(len); + FS_Read(buf, len, f); + FS_FCloseFile(f); + + if (!rsa_keypair_from_sexp(&cls.rsa.public_key, &cls.rsa.private_key, 0, len, buf)) + { + memset(buf, 0, len); + Z_Free(buf); + CL_UnloadRSAKeypair(); + Com_Error(ERR_FATAL, "Invalid RSA private key found."); + return; + } + + memset(buf, 0, len); + Z_Free(buf); + + len = FS_SV_FOpenFileRead(RSA_PUBLIC_KEY_FILE, &f); + if (!f || len < 1) CL_WriteRSAPublicKey(); + if (f) FS_FCloseFile(f); + + Com_DPrintf("RSA private key loaded\n"); +} + +/* +================= +CL_CheckForResend + +Resend a connect message if the last one has timed out +================= +*/ +static void CL_CheckForResend(void) +{ + int port; + char info[MAX_INFO_STRING]; + char data[MAX_MSGLEN]; + + // don't send anything if playing back a demo + if (clc.demoplaying) + { + return; + } + + // resend if we haven't gotten a reply yet + if (clc.state != CA_CONNECTING && clc.state != CA_CHALLENGING) + { + return; + } + + if (cls.realtime - clc.connectTime < RETRANSMIT_TIMEOUT) + { + return; + } + + clc.connectTime = cls.realtime; // for retransmit requests + clc.connectPacketCount++; + + switch (clc.state) + { + case CA_CONNECTING: + // requesting a challenge + + // The challenge request shall be followed by a client challenge so no malicious server can hijack this + // connection. + // Add the gamename so the server knows we're running the correct game or can reject the client + // with a meaningful message + if (clc.serverAddress.alternateProtocol == 2) + { + Com_sprintf(data, sizeof(data), "getchallenge"); + } + else if (clc.serverAddress.alternateProtocol == 1) + { + Com_sprintf(data, sizeof(data), "getchallenge %d", clc.challenge); + } + else + Com_sprintf(data, sizeof(data), "getchallenge %d %s", clc.challenge, com_gamename->string); + + NET_OutOfBandPrint(NS_CLIENT, clc.serverAddress, "%s", data); + break; + + case CA_CHALLENGING: + // sending back the challenge + port = Cvar_VariableValue("net_qport"); + + Q_strncpyz(info, Cvar_InfoString(CVAR_USERINFO), sizeof(info)); + Info_SetValueForKey(info, "protocol", + va("%i", + (clc.serverAddress.alternateProtocol == 0 ? PROTOCOL_VERSION + : clc.serverAddress.alternateProtocol == 1 ? 70 : 69))); + Info_SetValueForKey(info, "qport", va("%i", port)); + Info_SetValueForKey(info, "challenge", va("%i", clc.challenge)); + + if (cl_rsaAuth->integer && clc.sendSignature) + { + char public_key[RSA_STRING_LENGTH]; + char signature[RSA_STRING_LENGTH]; + struct sha256_ctx sha256_hash; + mpz_t n; + + Info_SetValueForKey(info, "challenge2", clc.challenge2); + + sha256_init(&sha256_hash); + sha256_update(&sha256_hash, strlen(info), (uint8_t *)info); + + mpz_init(n); + rsa_sha256_sign(&cls.rsa.private_key, &sha256_hash, n); + mpz_get_str(signature, 16, n); + mpz_clear(n); + + mpz_get_str(public_key, 16, cls.rsa.public_key.n); + + Com_sprintf(data, sizeof(data), "connect \"%s\" %s %s", info, public_key, signature); + } + else + { + Com_sprintf(data, sizeof(data), "connect \"%s\"", info); + } + + NET_OutOfBandData(NS_CLIENT, clc.serverAddress, (byte *)data, strlen(data)); + // the most current userinfo has been sent, so watch for any + // newer changes to userinfo variables + cvar_modifiedFlags &= ~CVAR_USERINFO; + break; + + default: + Com_Error(ERR_FATAL, "CL_CheckForResend: bad clc.state"); + } +} + +/* +===================== +CL_MapLoading + +A local server is starting to load a map, so update the +screen to let the user know about it, then dump all client +memory on the hunk from cgame, ui, and renderer +===================== +*/ +void CL_MapLoading(void) +{ + if (com_dedicated->integer) + { + clc.state = CA_DISCONNECTED; + Key_SetCatcher(KEYCATCH_CONSOLE); + CL_ProtocolSpecificCommandsInit(); + return; + } + + if (!com_cl_running->integer) + { + return; + } + + Key_SetCatcher(Key_GetCatcher() & KEYCATCH_CONSOLE); + + // if we are already connected to the local host, stay connected + if (clc.state >= CA_CONNECTED && !Q_stricmp(clc.servername, "localhost")) + { + clc.state = CA_CONNECTED; // so the connect screen is drawn + ::memset(cls.updateInfoString, 0, sizeof(cls.updateInfoString)); + ::memset(clc.serverMessage, 0, sizeof(clc.serverMessage)); + ::memset(&cl.gameState, 0, sizeof(cl.gameState)); + clc.lastPacketSentTime = -9999; + SCR_UpdateScreen(); + } + else + { + CL_Disconnect(true); + Q_strncpyz(clc.servername, "localhost", sizeof(clc.servername)); + clc.state = CA_CHALLENGING; // so the connect screen is drawn + Key_SetCatcher(Key_GetCatcher() & KEYCATCH_CONSOLE); + SCR_UpdateScreen(); + clc.connectTime = -RETRANSMIT_TIMEOUT; + NET_StringToAdr(clc.servername, &clc.serverAddress, NA_UNSPEC); + // we don't need a challenge on the localhost + + CL_CheckForResend(); + } +} + +/* +=================== +CL_MotdPacket + +=================== +*/ +static void CL_MotdPacket(netadr_t from, const char *info) +{ + const char *v; + + // if not from our server, ignore it + if (!NET_CompareAdr(from, cls.updateServer)) + { + Com_DPrintf("MOTD packet from unexpected source\n"); + return; + } + + Com_DPrintf("MOTD packet: %s\n", info); + while (*info != '\\') info++; + + // check challenge + v = Info_ValueForKey(info, "challenge"); + if (strcmp(v, cls.updateChallenge)) + { + Com_DPrintf( + "MOTD packet mismatched challenge: " + "'%s' != '%s'\n", + v, cls.updateChallenge); + return; + } + + v = Info_ValueForKey(info, "motd"); + + Q_strncpyz(cls.updateInfoString, info, sizeof(cls.updateInfoString)); + Cvar_Set("cl_motdString", v); +} + +/* +=================== +CL_InitServerInfo +=================== +*/ +static void CL_InitServerInfo(serverInfo_t *server, netadr_t *address) +{ + server->adr = *address; + server->clients = 0; + server->hostName[0] = '\0'; + server->mapName[0] = '\0'; + server->label[0] = '\0'; + server->maxClients = 0; + server->maxPing = 0; + server->minPing = 0; + server->ping = -1; + server->game[0] = '\0'; + server->gameType = 0; + server->netType = 0; +} + +/* +=================== +CL_GSRSequenceInformation + +Parses this packet's index and the number of packets from a master server's +response. Updates the packet count and returns the index. Advances the data +pointer as appropriate (but only when parsing was successful) + +The sequencing information isn't terribly useful at present (we can skip +duplicate packets, but we don't bother to make sure we've got all of them). +=================== +*/ +static int CL_GSRSequenceInformation(int alternateProtocol, byte **data) +{ + char *p = (char *)*data, *e; + int ind, num; + // '\0'-delimited fields: this packet's index, total number of packets + if (*p++ != '\0') return -1; + + ind = strtol(p, (char **)&e, 10); + if (*e++ != '\0') return -1; + + num = strtol(e, (char **)&p, 10); + if (*p++ != '\0') return -1; + + if (num <= 0 || ind <= 0 || ind > num) return -1; // nonsensical response + + if (cls.numAlternateMasterPackets[alternateProtocol] > 0 && num != cls.numAlternateMasterPackets[alternateProtocol]) + { + // Assume we sent two getservers and somehow they changed in + // between - only use the results that arrive later + Com_DPrintf("Master changed its mind about%s packet count!\n", + (alternateProtocol == 0 ? "" : alternateProtocol == 1 ? " alternate-1" : " alternate-2")); + cls.receivedAlternateMasterPackets[alternateProtocol] = 0; + // cls.numglobalservers = 0; + // cls.numGlobalServerAddresses = 0; + } + cls.numAlternateMasterPackets[alternateProtocol] = num; + + // successfully parsed + *data = (byte *)p; + return ind; +} + +/* +=================== +CL_GSRFeaturedLabel + +Parses from the data an arbitrary text string labelling the servers in the +following getserversresponse packet. +The result is copied to *buf, and *data is advanced as appropriate +=================== +*/ +static void CL_GSRFeaturedLabel(byte **data, char *buf, int size) +{ + char *l = buf; + + // copy until '\0' which indicates field break + // or slash which indicates beginning of server list + while (**data && **data != '\\' && **data != '/') + { + if (l < &buf[size - 1]) + *l = **data; + else if (l == &buf[size - 1]) + Com_DPrintf(S_COLOR_YELLOW + "Warning: " + "CL_GSRFeaturedLabel: overflow\n"); + l++, (*data)++; + } + + if (l < &buf[size - 1]) + *l = '\0'; + else + buf[size - 1] = '\0'; +} + +#define MAX_SERVERSPERPACKET 256 + +/* +=================== +CL_ServersResponsePacket +=================== +*/ +static void CL_ServersResponsePacket(const netadr_t *from, msg_t *msg, bool extended) +{ + int i, count, total; + netadr_t addresses[MAX_SERVERSPERPACKET]; + int numservers; + byte *buffptr; + byte *buffend; + char label[MAX_FEATLABEL_CHARS] = ""; + + Com_DPrintf("CL_ServersResponsePacket from %s %s\n", + NET_AdrToStringwPort(*from), + extended ? " (extended)" : ""); + + if (cls.numglobalservers == -1) + { + // state to detect lack of servers or lack of response + cls.numglobalservers = 0; + cls.numGlobalServerAddresses = 0; + for (i = 0; i < 3; ++i) + { + cls.numAlternateMasterPackets[i] = 0; + cls.receivedAlternateMasterPackets[i] = 0; + } + } + + // parse through server response string + numservers = 0; + buffptr = msg->data; + buffend = buffptr + msg->cursize; + + // skip header + buffptr += 4; + + // advance to initial token + // I considered using strchr for this but I don't feel like relying + // on its behaviour with '\0' + while (*buffptr && *buffptr != '\\' && *buffptr != '/') + { + buffptr++; + + if (buffptr >= buffend) break; + } + + if (*buffptr == '\0') + { + int ind = CL_GSRSequenceInformation(from->alternateProtocol, &buffptr); + if (ind >= 0) + { + // this denotes the start of new-syntax stuff + // have we already received this packet? + if (cls.receivedAlternateMasterPackets[from->alternateProtocol] & (1 << (ind - 1))) + { + Com_DPrintf( + "CL_ServersResponsePacket: " + "received packet %d again, ignoring\n", + ind); + return; + } + // TODO: detect dropped packets and make another + // request + Com_DPrintf( + "CL_ServersResponsePacket:%s packet " + "%d of %d\n", + (from->alternateProtocol == 0 ? "" : from->alternateProtocol == 1 ? " alternate-1" : " alternate-2"), + ind, cls.numAlternateMasterPackets[from->alternateProtocol]); + cls.receivedAlternateMasterPackets[from->alternateProtocol] |= (1 << (ind - 1)); + + CL_GSRFeaturedLabel(&buffptr, label, sizeof(label)); + } + // now skip to the server list + for (; buffptr < buffend && *buffptr != '\\' && *buffptr != '/'; buffptr++) + ; + } + + while (buffptr + 1 < buffend) + { + // IPv4 address + if (*buffptr == '\\') + { + buffptr++; + + if (buffend - buffptr < sizeof(addresses[numservers].ip) + sizeof(addresses[numservers].port) + 1) break; + + for (i = 0; i < sizeof(addresses[numservers].ip); i++) addresses[numservers].ip[i] = *buffptr++; + + addresses[numservers].type = NA_IP; + } + // IPv6 address, if it's an extended response + else if (extended && *buffptr == '/') + { + buffptr++; + + if (buffend - buffptr < sizeof(addresses[numservers].ip6) + sizeof(addresses[numservers].port) + 1) break; + + for (i = 0; i < sizeof(addresses[numservers].ip6); i++) addresses[numservers].ip6[i] = *buffptr++; + + addresses[numservers].type = NA_IP6; + addresses[numservers].scope_id = from->scope_id; + } + else + // syntax error! + break; + + // parse out port + addresses[numservers].port = (*buffptr++) << 8; + addresses[numservers].port += *buffptr++; + addresses[numservers].port = BigShort(addresses[numservers].port); + + // syntax check + if (*buffptr != '\\' && *buffptr != '/') break; + + addresses[numservers].alternateProtocol = from->alternateProtocol; + + numservers++; + if (numservers >= MAX_SERVERSPERPACKET) break; + } + + count = cls.numglobalservers; + + for (i = 0; i < numservers && count < MAX_GLOBAL_SERVERS; i++) + { + // build net address + serverInfo_t *server = &cls.globalServers[count]; + + CL_InitServerInfo(server, &addresses[i]); + Q_strncpyz(server->label, label, sizeof(server->label)); + // advance to next slot + count++; + } + + // if getting the global list + if (count >= MAX_GLOBAL_SERVERS && cls.numGlobalServerAddresses < MAX_GLOBAL_SERVERS) + { + // if we couldn't store the servers in the main list anymore + for (; i < numservers && cls.numGlobalServerAddresses < MAX_GLOBAL_SERVERS; i++) + { + // just store the addresses in an additional list + cls.globalServerAddresses[cls.numGlobalServerAddresses++] = addresses[i]; + } + } + + cls.numglobalservers = count; + total = count + cls.numGlobalServerAddresses; + + Com_Printf("%d servers parsed (total %d)\n", numservers, total); +} +/* +================== +CL_CheckTimeout + +================== +*/ +static void CL_CheckTimeout(void) +{ + // + // check timeout + // + if ((!CL_CheckPaused() || !sv_paused->integer) && clc.state >= CA_CONNECTED && clc.state != CA_CINEMATIC && + cls.realtime - clc.lastPacketTime > cl_timeout->value * 1000) + { + if (++cl.timeoutcount > 5) + { // timeoutcount saves debugger + Com_Printf("\nServer connection timed out.\n"); + CL_Disconnect(true); + return; + } + } + else + { + cl.timeoutcount = 0; + } +} + +/* +================== +CL_CheckPaused +Check whether client has been paused. +================== +*/ +bool CL_CheckPaused(void) +{ // if cl_paused->modified is set, the cvar has only been changed in + // this frame. Keep paused in this frame to ensure the server doesn't + // lag behind. + if (cl_paused->integer || cl_paused->modified) return true; + + return false; +} + +//============================================================================ + +/* +================== +CL_CheckUserinfo + +================== +*/ +static void CL_CheckUserinfo(void) +{ + // don't add reliable commands when not yet connected + if (clc.state < CA_CONNECTED) return; + + // don't overflow the reliable command buffer when paused + if (CL_CheckPaused()) return; + + // send a reliable userinfo update if needed + if (cvar_modifiedFlags & CVAR_USERINFO) + { + cvar_modifiedFlags &= ~CVAR_USERINFO; + CL_AddReliableCommand(va("userinfo \"%s\"", Cvar_InfoString(CVAR_USERINFO)), false); + } +} + +/* +================== +CL_Frame + +================== +*/ +void CL_Frame(int msec) +{ + if (!com_cl_running->integer) return; + + // We may have a download prompt ready + if ((com_downloadPrompt->integer & DLP_TYPE_MASK) && !(com_downloadPrompt->integer & DLP_PROMPTED)) + { + Com_Printf("Download prompt returned %d\n", com_downloadPrompt->integer); + CL_NextDownload(); + } + else if (com_downloadPrompt->integer & DLP_SHOW) + { + // If the UI VM does not support the download prompt, we need to catch + // the prompt here and replicate regular behavior. + // One frame will always run between requesting and showing the prompt. + + if (com_downloadPrompt->integer & DLP_STALE) + { + Com_Printf("WARNING: UI VM does not support download prompt\n"); + Cvar_Set("com_downloadPrompt", va("%d", DLP_IGNORE)); + CL_NextDownload(); + } + else + { + Cvar_Set("com_downloadPrompt", va("%d", com_downloadPrompt->integer | DLP_STALE)); + } + } + + if (clc.downloadCURLM) + { + CL_cURL_PerformDownload(); + // we can't process frames normally when in disconnected download mode + // since the ui vm expects clc.state to be CA_CONNECTED + if (clc.cURLDisconnected) + { + cls.realFrametime = msec; + cls.frametime = msec; + cls.realtime += cls.frametime; + + SCR_UpdateScreen(); + S_Update(); + Con_RunConsole(); + + cls.framecount++; + return; + } + } + + if (clc.state == CA_DISCONNECTED && !(Key_GetCatcher() & KEYCATCH_UI) && !com_sv_running->integer && cls.ui) + { + // if disconnected, bring up the menu + S_StopAllSounds(); + VM_Call(cls.ui, UI_SET_ACTIVE_MENU - (cls.uiInterface == 2 ? 2 : 0), UIMENU_MAIN); + } + + // if recording an avi, lock to a fixed fps + if (CL_VideoRecording() && cl_aviFrameRate->integer && msec) + { + // save the current screen + if (clc.state == CA_ACTIVE || cl_forceavidemo->integer) + { + float fps = MIN(cl_aviFrameRate->value * com_timescale->value, 1000.0f); + float frameDuration = MAX(1000.0f / fps, 1.0f) + clc.aviVideoFrameRemainder; + + CL_TakeVideoFrame(); + + msec = (int)frameDuration; + clc.aviVideoFrameRemainder = frameDuration - msec; + } + } + + if (cl_autoRecordDemo->integer) + { + if (clc.state == CA_ACTIVE && !clc.demorecording && !clc.demoplaying) + { + // If not recording a demo, and we should be, start one + qtime_t now; + Com_RealTime(&now); + + const char *nowString = va("%04d%02d%02d%02d%02d%02d", 1900 + now.tm_year, 1 + now.tm_mon, now.tm_mday, + now.tm_hour, now.tm_min, now.tm_sec); + + char serverName[MAX_OSPATH]; + Q_strncpyz(serverName, clc.servername, MAX_OSPATH); + + // Replace the ":" in the address as it is not a valid + // file name character + char *p = strstr(serverName, ":"); + if (p) *p = '.'; + + char mapName[MAX_QPATH]; + Q_strncpyz(mapName, COM_SkipPath(cl.mapname), sizeof(cl.mapname)); + COM_StripExtension(mapName, mapName, sizeof(mapName)); + Cbuf_ExecuteText(EXEC_NOW, va("record \"%s-%s-%s\"\n", nowString, serverName, mapName)); + } + else if (clc.state != CA_ACTIVE && clc.demorecording) + { + // Recording, but not CA_ACTIVE, so stop recording + CL_StopRecord_f(); + } + } + + // save the msec before checking pause + cls.realFrametime = msec; + + // decide the simulation time + cls.frametime = msec; + + cls.realtime += cls.frametime; + + if (cl_timegraph->integer) + { + SCR_DebugGraph(cls.realFrametime * 0.25); + } + + // see if we need to update any userinfo + CL_CheckUserinfo(); + + // if we haven't gotten a packet in a long time, drop the connection + CL_CheckTimeout(); + + // send intentions now + CL_SendCmd(); + + // resend a connection request if necessary + CL_CheckForResend(); + + // decide on the serverTime to render + CL_SetCGameTime(); + + // update the screen + SCR_UpdateScreen(); + + // update audio + S_Update(); + +#ifdef USE_VOIP + CL_CaptureVoip(); +#endif + +#ifdef USE_MUMBLE + CL_UpdateMumble(); +#endif + + // advance local effects for next frame + SCR_RunCinematic(); + + Con_RunConsole(); + + cls.framecount++; +} + +//============================================================================ + +/* +================ +CL_RefPrintf + +DLL glue +================ +*/ +static __attribute__((format(printf, 2, 3))) void QDECL CL_RefPrintf(int print_level, const char *fmt, ...) +{ + va_list argptr; + char msg[MAXPRINTMSG]; + + va_start(argptr, fmt); + Q_vsnprintf(msg, sizeof(msg), fmt, argptr); + va_end(argptr); + + switch (print_level) + { + case PRINT_ALL: + Com_Printf("%s", msg); + break; + + case PRINT_WARNING: + Com_Printf(S_COLOR_YELLOW "%s", msg); + break; + + case PRINT_DEVELOPER: + Com_DPrintf(S_COLOR_RED "%s", msg); + break; + } +} + +/* +============ +CL_ShutdownRef +============ +*/ +static void CL_ShutdownRef(void) +{ + if (re.Shutdown) re.Shutdown(true); + + ::memset(&re, 0, sizeof(re)); + +#ifdef USE_RENDERER_DLOPEN + if (rendererLib) + { + Sys_UnloadLibrary(rendererLib); + rendererLib = NULL; + } +#endif +} + +/* +===================== +CL_ShutdownAll +===================== +*/ +void CL_ShutdownAll(bool shutdownRef) +{ + if (CL_VideoRecording()) CL_CloseAVI(); + + if (clc.demorecording) CL_StopRecord_f(); + + CL_cURL_Shutdown(); + + // clear sounds + S_DisableSounds(); + // shutdown CGame + CL_ShutdownCGame(); + // shutdown UI + CL_ShutdownUI(); + + // shutdown the renderer + if (shutdownRef) + CL_ShutdownRef(); + else if (re.Shutdown) + re.Shutdown(false); // don't destroy window or context + + cls.uiStarted = false; + cls.cgameStarted = false; + cls.rendererStarted = false; + cls.soundRegistered = false; +} + +/* +================= +CL_ClearMemory + +Called by Com_GameRestart +================= +*/ +static void CL_ClearMemory(bool shutdownRef) +{ + // shutdown all the client stuff + CL_ShutdownAll(shutdownRef); + + // if not running a server clear the whole hunk + if (!com_sv_running->integer) + { + // clear the whole hunk + Hunk_Clear(); + // clear collision map data + CM_ClearMap(); + } + else + { + // clear all the client data on the hunk + Hunk_ClearToMark(); + } +} + +/* +================= +CL_FlushMemory + +Called by CL_MapLoading, CL_Connect_f, CL_PlayDemo_f, and CL_ParseGamestate the only +ways a client gets into a game +Also called by Com_Error +================= +*/ +void CL_FlushMemory(void) +{ + CL_ClearMemory(false); + CL_StartHunkUsers(false); +} + +/* +================= +CL_Vid_Restart_f + +Restart the video subsystem + +we also have to reload the UI and CGame because the renderer +doesn't know what graphics to reload +================= +*/ +static void CL_Vid_Restart_f(void) +{ + // Settings may have changed so stop recording now + if (CL_VideoRecording()) + { + CL_CloseAVI(); + } + + if (clc.demorecording) CL_StopRecord_f(); + + // don't let them loop during the restart + S_StopAllSounds(); + + if (!FS_ConditionalRestart(clc.checksumFeed, true)) + { + // if not running a server clear the whole hunk + if (com_sv_running->integer) + { + // clear all the client data on the hunk + Hunk_ClearToMark(); + } + else + { + // clear the whole hunk + Hunk_Clear(); + } + + // shutdown the UI + CL_ShutdownUI(); + // shutdown the CGame + CL_ShutdownCGame(); + // shutdown the renderer and clear the renderer interface + CL_ShutdownRef(); + // client is no longer pure untill new checksums are sent + CL_ResetPureClientAtServer(); + // clear pak references + FS_ClearPakReferences(FS_UI_REF | FS_CGAME_REF); + // reinitialize the filesystem if the game directory or checksum has changed + + cls.rendererStarted = false; + cls.uiStarted = false; + cls.cgameStarted = false; + cls.soundRegistered = false; + + // unpause so the cgame definately gets a snapshot and renders a frame + Cvar_Set("cl_paused", "0"); + + // initialize the renderer interface + CL_InitRef(); + + // startup all the client stuff + CL_StartHunkUsers(false); + + // start the cgame if connected + if (clc.state > CA_CONNECTED && clc.state != CA_CINEMATIC) + { + cls.cgameStarted = true; + CL_InitCGame(); + // send pure checksums + CL_SendPureChecksums(); + } + } +} + +/* +================= +CL_Snd_Restart_f + +Restart the sound subsystem +The cgame and game must also be forced to restart because +handles will be invalid +================= +*/ +static void CL_Snd_Restart_f(void) +{ + CL_Snd_Shutdown(); + // sound will be reinitialized by vid_restart + CL_Vid_Restart_f(); +} + + +/* +============ +CL_InitRenderer +============ +*/ +static void CL_InitRenderer(void) +{ + // this sets up the renderer and calls R_Init + re.BeginRegistration(&cls.glconfig); + + // load character sets + cls.charSetShader = re.RegisterShader("gfx/2d/bigchars"); + cls.whiteShader = re.RegisterShader("white"); + cls.consoleShader = re.RegisterShader("console"); + g_console_field_width = cls.glconfig.vidWidth / SMALLCHAR_WIDTH - 2; + g_consoleField.widthInChars = g_console_field_width; +} + +/* +============================ +CL_StartHunkUsers + +After the server has cleared the hunk, these will need to be restarted +This is the only place that any of these functions are called from +============================ +*/ +void CL_StartHunkUsers(bool rendererOnly) +{ + if (!com_cl_running) + { + return; + } + + if (!com_cl_running->integer) + { + return; + } + + if (!cls.rendererStarted) + { + cls.rendererStarted = true; + CL_InitRenderer(); + } + + if (rendererOnly) + { + return; + } + + if (!cls.soundStarted) + { + cls.soundStarted = true; + S_Init(); + } + + if (!cls.soundRegistered) + { + cls.soundRegistered = true; + S_BeginRegistration(); + } + + if (com_dedicated->integer) + { + return; + } + + if (!cls.uiStarted) + { + cls.uiStarted = true; + CL_InitUI(); + } +} + +/* +============ +CL_RefMalloc +============ +*/ +static void *CL_RefMalloc(int size) { return Z_TagMalloc(size, TAG_RENDERER); } + +/* +============ +CL_ScaledMilliseconds +============ +*/ +int CL_ScaledMilliseconds(void) { return Sys_Milliseconds() * com_timescale->value; } + +//=========================================================================================== + +static void CL_SetModel_f(void) +{ + char name[256]; + + const char *arg = Cmd_Argv(1); + if (arg[0]) + { + Cvar_Set("model", arg); + Cvar_Set("headmodel", arg); + } + else + { + Cvar_VariableStringBuffer("model", name, sizeof(name)); + Com_Printf("model is set to %s\n", name); + } +} + +//=========================================================================================== + +/* +=============== +CL_Video_f + +video +video [filename] +=============== +*/ +static void CL_Video_f(void) +{ + char filename[MAX_OSPATH]; + + if (!clc.demoplaying) + { + Com_Printf("The video command can only be used when playing back demos\n"); + return; + } + + if (Cmd_Argc() == 2) + { + // explicit filename + Com_sprintf(filename, MAX_OSPATH, "videos/%s.avi", Cmd_Argv(1)); + } + else + { + int i, last; + + // scan for a free filename + for (i = 0; i <= 9999; i++) + { + int a, b, c, d; + + last = i; + + a = last / 1000; + last -= a * 1000; + b = last / 100; + last -= b * 100; + c = last / 10; + last -= c * 10; + d = last; + + Com_sprintf(filename, MAX_OSPATH, "videos/video%d%d%d%d.avi", a, b, c, d); + + if (!FS_FileExists(filename)) break; // file doesn't exist + } + + if (i > 9999) + { + Com_Printf(S_COLOR_RED "ERROR: no free file names to create video\n"); + return; + } + } + + CL_OpenAVIForWriting(filename); +} + +/* +=============== +CL_StopVideo_f +=============== +*/ +static void CL_StopVideo_f(void) { CL_CloseAVI(); } +/* +=============== +CL_GenerateQKey + +test to see if a valid QKEY_FILE exists. If one does not, try to generate +it by filling it with 2048 bytes of random data. +=============== +*/ +static void CL_GenerateQKey(void) +{ + int len = 0; + unsigned char buff[QKEY_SIZE]; + fileHandle_t f; + + len = FS_SV_FOpenFileRead(QKEY_FILE, &f); + FS_FCloseFile(f); + if (len == QKEY_SIZE) + { + Com_Printf("QKEY found.\n"); + return; + } + else + { + if (len > 0) + { + Com_Printf("QKEY file size != %d, regenerating\n", QKEY_SIZE); + } + + Com_Printf("QKEY building random string\n"); + Com_RandomBytes(buff, sizeof(buff)); + + f = FS_SV_FOpenFileWrite(QKEY_FILE); + if (!f) + { + Com_Printf("QKEY could not open %s for write\n", QKEY_FILE); + return; + } + FS_Write(buff, sizeof(buff), f); + FS_FCloseFile(f); + Com_Printf("QKEY generated\n"); + } +} + +static void CL_SetServerInfo(serverInfo_t *server, const char *info, int ping) +{ + if (server) + { + if (info) + { + server->clients = atoi(Info_ValueForKey(info, "clients")); + Q_strncpyz(server->hostName, Info_ValueForKey(info, "hostname"), MAX_HOSTNAME_LENGTH); + Q_strncpyz(server->mapName, Info_ValueForKey(info, "mapname"), MAX_NAME_LENGTH); + server->maxClients = atoi(Info_ValueForKey(info, "sv_maxclients")); + server->gameType = atoi(Info_ValueForKey(info, "gametype")); + server->netType = atoi(Info_ValueForKey(info, "nettype")); + server->minPing = atoi(Info_ValueForKey(info, "minping")); + server->maxPing = atoi(Info_ValueForKey(info, "maxping")); + const char *game = Info_ValueForKey(info, "game"); + Q_strncpyz(server->game, (game[0]) ? game : BASEGAME, MAX_NAME_LENGTH); + } + server->ping = ping; + } +} + +static void CL_SetServerInfoByAddress(netadr_t from, const char *info, int ping) +{ + int i; + + for (i = 0; i < MAX_OTHER_SERVERS; i++) + { + if (NET_CompareAdr(from, cls.localServers[i].adr)) + { + CL_SetServerInfo(&cls.localServers[i], info, ping); + } + } + + for (i = 0; i < MAX_GLOBAL_SERVERS; i++) + { + if (NET_CompareAdr(from, cls.globalServers[i].adr)) + { + CL_SetServerInfo(&cls.globalServers[i], info, ping); + } + } + + for (i = 0; i < MAX_OTHER_SERVERS; i++) + { + if (NET_CompareAdr(from, cls.favoriteServers[i].adr)) + { + CL_SetServerInfo(&cls.favoriteServers[i], info, ping); + } + } +} + +/* +=================== +CL_ServerInfoPacket +=================== +*/ +static void CL_ServerInfoPacket(netadr_t from, msg_t *msg) +{ + int i, type; + char info[MAX_INFO_STRING]; + char *infoString; + int prot; + char *gamename; + bool gameMismatch; + + infoString = MSG_ReadString(msg); + + if (from.alternateProtocol == 0) + { + // if this isn't the correct gamename, ignore it + gamename = Info_ValueForKey(infoString, "gamename"); + + gameMismatch = !*gamename || strcmp(gamename, com_gamename->string) != 0; + + if (gameMismatch) + { + Com_DPrintf("Game mismatch in info packet: %s\n", infoString); + return; + } + } + + // if this isn't the correct protocol version, ignore it + prot = atoi(Info_ValueForKey(infoString, "protocol")); + if (prot != (from.alternateProtocol == 0 ? PROTOCOL_VERSION : from.alternateProtocol == 1 ? 70 : 69)) + { + Com_DPrintf("Different protocol info packet: %s\n", infoString); + return; + } + + // iterate servers waiting for ping response + for (i = 0; i < MAX_PINGREQUESTS; i++) + { + if (cl_pinglist[i].adr.port && !cl_pinglist[i].time && NET_CompareAdr(from, cl_pinglist[i].adr)) + { + // calc ping time + cl_pinglist[i].time = Sys_Milliseconds() - cl_pinglist[i].start; + Com_DPrintf("ping time %dms from %s\n", cl_pinglist[i].time, NET_AdrToString(from)); + + // save of info + Q_strncpyz(cl_pinglist[i].info, infoString, sizeof(cl_pinglist[i].info)); + + // tack on the net type + // NOTE: make sure these types are in sync with the netnames strings in the UI + switch (from.type) + { + case NA_BROADCAST: + case NA_IP: + type = 1; + break; + case NA_IP6: + type = 2; + break; + default: + type = 0; + break; + } + Info_SetValueForKey(cl_pinglist[i].info, "nettype", va("%d", type)); + CL_SetServerInfoByAddress(from, infoString, cl_pinglist[i].time); + + return; + } + } + + // if not just sent a local broadcast or pinging local servers + if (cls.pingUpdateSource != AS_LOCAL) + { + return; + } + + for (i = 0; i < MAX_OTHER_SERVERS; i++) + { + // empty slot + if (cls.localServers[i].adr.port == 0) + { + break; + } + + // avoid duplicate + if (NET_CompareAdr(from, cls.localServers[i].adr)) + { + return; + } + } + + if (i == MAX_OTHER_SERVERS) + { + Com_DPrintf("MAX_OTHER_SERVERS hit, dropping infoResponse\n"); + return; + } + + // add this to the list + cls.numlocalservers = i + 1; + CL_InitServerInfo(&cls.localServers[i], &from); + + Q_strncpyz(info, MSG_ReadString(msg), MAX_INFO_STRING); + if (strlen(info)) + { + if (info[strlen(info) - 1] != '\n') + { + Q_strcat(info, sizeof(info), "\n"); + } + Com_Printf("%s: %s", NET_AdrToStringwPort(from), info); + } +} + +/* +=================== +CL_ServerStatusResponse +=================== +*/ +static void CL_ServerStatusResponse(netadr_t from, msg_t *msg) +{ + char info[MAX_INFO_STRING]; + int i, l, score, ping; + int len; + serverStatus_t *serverStatus; + + serverStatus = NULL; + for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) + { + if (NET_CompareAdr(from, cl_serverStatusList[i].address)) + { + serverStatus = &cl_serverStatusList[i]; + break; + } + } + // if we didn't request this server status + if (!serverStatus) + { + return; + } + + const char *s = MSG_ReadStringLine(msg); + + len = 0; + Com_sprintf(&serverStatus->string[len], sizeof(serverStatus->string) - len, "%s", s); + + if (serverStatus->print) + { + Com_Printf("Server settings:\n"); + // print cvars + while (*s) + { + for (i = 0; i < 2 && *s; i++) + { + if (*s == '\\') s++; + l = 0; + while (*s) + { + info[l++] = *s; + if (l >= MAX_INFO_STRING - 1) break; + s++; + if (*s == '\\') + { + break; + } + } + info[l] = '\0'; + if (i) + { + Com_Printf("%s\n", info); + } + else + { + Com_Printf("%-24s", info); + } + } + } + } + + len = strlen(serverStatus->string); + Com_sprintf(&serverStatus->string[len], sizeof(serverStatus->string) - len, "\\"); + + if (serverStatus->print) + { + Com_Printf("\nPlayers:\n"); + Com_Printf("num: score: ping: name:\n"); + } + for (i = 0, s = MSG_ReadStringLine(msg); *s; s = MSG_ReadStringLine(msg), i++) + { + len = strlen(serverStatus->string); + Com_sprintf(&serverStatus->string[len], sizeof(serverStatus->string) - len, "\\%s", s); + + if (serverStatus->print) + { + score = ping = 0; + sscanf(s, "%d %d", &score, &ping); + s = strchr(s, ' '); + if (s) s = strchr(s + 1, ' '); + if (s) + s++; + else + s = "unknown"; + Com_Printf("%-2d %-3d %-3d %s\n", i, score, ping, s); + } + } + len = strlen(serverStatus->string); + Com_sprintf(&serverStatus->string[len], sizeof(serverStatus->string) - len, "\\"); + + serverStatus->time = Com_Milliseconds(); + serverStatus->address = from; + serverStatus->pending = false; + if (serverStatus->print) + { + serverStatus->retrieved = true; + } +} + +/* +================= +CL_ConnectionlessPacket + +Responses to broadcasts, etc +================= +*/ +static void CL_ConnectionlessPacket(netadr_t from, msg_t *msg) +{ + int challenge = 0; + + MSG_BeginReadingOOB(msg); + MSG_ReadLong(msg); // skip the -1 + + const char *s = MSG_ReadStringLine(msg); + + Cmd_TokenizeString(s); + + const char *c = Cmd_Argv(0); + + Com_DPrintf("CL packet %s: %s\n", NET_AdrToStringwPort(from), c); + + // challenge from the server we are connecting to + if (!Q_stricmp(c, "challengeResponse")) + { + int ver; + + if (clc.state != CA_CONNECTING) + { + Com_DPrintf("Unwanted challenge response received. Ignored.\n"); + return; + } + + const char *strver = Cmd_Argv(3); + if (*strver) + { + ver = atoi(strver); + + if (ver != PROTOCOL_VERSION) + { + Com_Printf(S_COLOR_YELLOW + "Warning: Server reports protocol version %d, we have %d. " + "Trying anyways.\n", + ver, PROTOCOL_VERSION); + } + } + if (clc.serverAddress.alternateProtocol == 0) + { + c = Cmd_Argv(2); + if (*c) challenge = atoi(c); + + if (!*c || challenge != clc.challenge) + { + Com_Printf("Bad challenge for challengeResponse. Ignored.\n"); + return; + } + } + + // start sending challenge response instead of challenge request packets + clc.challenge = atoi(Cmd_Argv(1)); + clc.state = CA_CHALLENGING; + clc.connectPacketCount = 0; + clc.connectTime = -99999; + + if (cl_rsaAuth->integer) + { + s = Cmd_Argv(4); + if (*s) + { + Q_strncpyz(clc.challenge2, s, sizeof(clc.challenge2)); + clc.sendSignature = true; + } + } + + // take this address as the new server address. This allows + // a server proxy to hand off connections to multiple servers + clc.serverAddress = from; + Com_DPrintf("challengeResponse: %d\n", clc.challenge); + return; + } + + // server connection + if (!Q_stricmp(c, "connectResponse")) + { + if (clc.state >= CA_CONNECTED) + { + Com_Printf("Dup connect received. Ignored.\n"); + return; + } + if (clc.state != CA_CHALLENGING) + { + Com_Printf("connectResponse packet while not connecting. Ignored.\n"); + return; + } + if (!NET_CompareAdr(from, clc.serverAddress)) + { + Com_Printf("connectResponse from wrong address. Ignored.\n"); + return; + } + + if (clc.serverAddress.alternateProtocol == 0) + { + c = Cmd_Argv(1); + + if (*c) + challenge = atoi(c); + else + { + Com_Printf("Bad connectResponse received. Ignored.\n"); + return; + } + + if (challenge != clc.challenge) + { + Com_Printf("ConnectResponse with bad challenge received. Ignored.\n"); + return; + } + } + + Netchan_Setup(clc.serverAddress.alternateProtocol, NS_CLIENT, &clc.netchan, from, + Cvar_VariableValue("net_qport"), clc.challenge); + + clc.state = CA_CONNECTED; + clc.lastPacketSentTime = -9999; // send first packet immediately + return; + } + + // server responding to an info broadcast + if (!Q_stricmp(c, "infoResponse")) + { + CL_ServerInfoPacket(from, msg); + return; + } + + // server responding to a get playerlist + if (!Q_stricmp(c, "statusResponse")) + { + CL_ServerStatusResponse(from, msg); + return; + } + + // echo request from server + if (!Q_stricmp(c, "echo")) + { + NET_OutOfBandPrint(NS_CLIENT, from, "%s", Cmd_Argv(1)); + return; + } + + // global MOTD from trem master + if (!Q_stricmp(c, "motd")) + { + CL_MotdPacket(from, s); + return; + } + + // echo request from server + if (!Q_stricmp(c, "print")) + { + s = MSG_ReadString(msg); + + Q_strncpyz(clc.serverMessage, s, sizeof(clc.serverMessage)); + + while (clc.serverMessage[strlen(clc.serverMessage) - 1] == '\n') + clc.serverMessage[strlen(clc.serverMessage) - 1] = '\0'; + + Com_Printf("%s", s); + + return; + } + + // list of servers sent back by a master server (classic) + if (!Q_strncmp(c, "getserversResponse", 18)) + { + CL_ServersResponsePacket(&from, msg, false); + + return; + } + + // list of servers sent back by a master server (extended) + if (!Q_strncmp(c, "getserversExtResponse", 21)) + { + CL_ServersResponsePacket(&from, msg, true); + return; + } + + Com_DPrintf("Unknown connectionless packet command.\n"); +} + +/* +================= +CL_PacketEvent + +A packet has arrived from the main event loop +================= +*/ +void CL_PacketEvent(netadr_t from, msg_t *msg) +{ + int headerBytes; + + clc.lastPacketTime = cls.realtime; + + if (msg->cursize >= 4 && *(int *)msg->data == -1) + { + CL_ConnectionlessPacket(from, msg); + return; + } + + if (clc.state < CA_CONNECTED) + { + return; // can't be a valid sequenced packet + } + + if (msg->cursize < 4) + { + Com_Printf("%s: Runt packet\n", NET_AdrToStringwPort(from)); + return; + } + + // + // packet from server + // + if (!NET_CompareAdr(from, clc.netchan.remoteAddress)) + { + Com_DPrintf("%s:sequenced packet without connection\n", NET_AdrToStringwPort(from)); + // FIXME: send a client disconnect? + return; + } + + if (!CL_Netchan_Process(&clc.netchan, msg)) + { + return; // out of order, duplicated, etc + } + + // the header is different lengths for reliable and unreliable messages + headerBytes = msg->readcount; + + // track the last message received so it can be returned in + // client messages, allowing the server to detect a dropped + // gamestate + clc.serverMessageSequence = LittleLong(*(int *)msg->data); + + clc.lastPacketTime = cls.realtime; + CL_ParseServerMessage(msg); + + // + // we don't know if it is ok to save a demo message until + // after we have parsed the frame + // + if (clc.demorecording && !clc.demowaiting) + { + CL_WriteDemoMessage(msg, headerBytes); + } +} + +/* +=================== +CL_GetServerStatus +=================== +*/ +static serverStatus_t *CL_GetServerStatus(netadr_t from) +{ + int i, oldest, oldestTime; + + for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) + { + if (NET_CompareAdr(from, cl_serverStatusList[i].address)) + { + return &cl_serverStatusList[i]; + } + } + for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) + { + if (cl_serverStatusList[i].retrieved) + { + return &cl_serverStatusList[i]; + } + } + oldest = -1; + oldestTime = 0; + for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) + { + if (oldest == -1 || cl_serverStatusList[i].startTime < oldestTime) + { + oldest = i; + oldestTime = cl_serverStatusList[i].startTime; + } + } + return &cl_serverStatusList[oldest]; +} + +/* +=================== +CL_ServerStatus +=================== +*/ +bool CL_ServerStatus(char *serverAddress, char *serverStatusString, int maxLen) +{ + int i; + netadr_t to; + serverStatus_t *serverStatus; + + // if no server address then reset all server status requests + if (!serverAddress) + { + for (i = 0; i < MAX_SERVERSTATUSREQUESTS; i++) + { + cl_serverStatusList[i].address.port = 0; + cl_serverStatusList[i].retrieved = true; + } + return false; + } + // get the address + if (!NET_StringToAdr(serverAddress, &to, NA_UNSPEC)) + { + return false; + } + serverStatus = CL_GetServerStatus(to); + // if no server status string then reset the server status request for this address + if (!serverStatusString) + { + serverStatus->retrieved = true; + return false; + } + + // if this server status request has the same address + if (NET_CompareAdr(to, serverStatus->address)) + { + // if we received a response for this server status request + if (!serverStatus->pending) + { + Q_strncpyz(serverStatusString, serverStatus->string, maxLen); + serverStatus->retrieved = true; + serverStatus->startTime = 0; + return true; + } + // resend the request regularly + else if (serverStatus->startTime < Com_Milliseconds() - cl_serverStatusResendTime->integer) + { + serverStatus->print = false; + serverStatus->pending = true; + serverStatus->retrieved = false; + serverStatus->time = 0; + serverStatus->startTime = Com_Milliseconds(); + NET_OutOfBandPrint(NS_CLIENT, to, "getstatus"); + return false; + } + } + // if retrieved + else if (serverStatus->retrieved) + { + serverStatus->address = to; + serverStatus->print = false; + serverStatus->pending = true; + serverStatus->retrieved = false; + serverStatus->startTime = Com_Milliseconds(); + serverStatus->time = 0; + NET_OutOfBandPrint(NS_CLIENT, to, "getstatus"); + return false; + } + return false; +} + +/* +================== +CL_LocalServers_f +================== +*/ +static void CL_LocalServers_f(void) +{ + const char *message; + int i, j; + netadr_t to; + + Com_Printf("Scanning for servers on the local network...\n"); + + // reset the list, waiting for response + cls.numlocalservers = 0; + cls.pingUpdateSource = AS_LOCAL; + + for (i = 0; i < MAX_OTHER_SERVERS; i++) + { + bool b = cls.localServers[i].visible; + ::memset(&cls.localServers[i], 0, sizeof(cls.localServers[i])); + cls.localServers[i].visible = b; + } + ::memset(&to, 0, sizeof(to)); + + // The 'xxx' in the message is a challenge that will be echoed back + // by the server. We don't care about that here, but master servers + // can use that to prevent spoofed server responses from invalid ip + message = "\377\377\377\377getinfo xxx"; + + // send each message twice in case one is dropped + for (i = 0; i < 2; i++) + { + // send a broadcast packet on each server port + // we support multiple server ports so a single machine + // can nicely run multiple servers + for (j = 0; j < NUM_SERVER_PORTS; j++) + { + to.port = BigShort((short)(PORT_SERVER + j)); + + to.type = NA_BROADCAST; + NET_SendPacket(NS_CLIENT, strlen(message), message, to); + to.type = NA_MULTICAST6; + NET_SendPacket(NS_CLIENT, strlen(message), message, to); + } + } +} + +/* +================== +CL_GlobalServers_f +================== +*/ +static void CL_GlobalServers_f(void) +{ + int netAlternateProtocols, a; + int i; + char command[1024]; + const char *masteraddress; + + int masterNum; + int count = Cmd_Argc(); + if ( count < 2 || (masterNum = atoi(Cmd_Argv(1))) < 0 || masterNum > MAX_MASTER_SERVERS ) + { + Com_Printf("usage: globalservers <master# 0-%d> [keywords]\n", MAX_MASTER_SERVERS); + return; + } + + netAlternateProtocols = Cvar_VariableIntegerValue("net_alternateProtocols"); + + for (a = 0; a < 3; ++a) + { + // indent + if (a == 0 && (netAlternateProtocols & NET_DISABLEPRIMPROTO)) continue; + if (a == 1 && !(netAlternateProtocols & NET_ENABLEALT1PROTO)) continue; + if (a == 2 && !(netAlternateProtocols & NET_ENABLEALT2PROTO)) continue; + + // request from all master servers + if ( masterNum == 0 ) + { + int numAddress = 0; + + for ( int i = 1; i <= MAX_MASTER_SERVERS; i++ ) + { + sprintf(command, "sv_master%d", i); + masteraddress = Cvar_VariableString(command); + + if(!*masteraddress) + continue; + + numAddress++; + + Com_sprintf(command, sizeof(command), "globalservers %d %s %s\n", i, Cmd_Argv(2), Cmd_ArgsFrom(3)); + Cbuf_AddText(command); + } + + if ( !numAddress ) + Com_Printf("CL_GlobalServers_f: Error: No master server addresses.\n"); + + return; + } + + sprintf(command, "sv_%smaster%d", (a == 0 ? "" : a == 1 ? "alt1" : "alt2"), masterNum); + masteraddress = Cvar_VariableString(command); + + if (!*masteraddress) + { + Com_Printf("CL_GlobalServers_f: Error: No%s master server address given.\n", + (a == 0 ? "" : a == 1 ? " alternate-1" : " alternate-2")); + continue; + } + + // reset the list, waiting for response + // -1 is used to distinguish a "no response" + netadr_t to; + int i = NET_StringToAdr(masteraddress, &to, NA_UNSPEC); + + if ( i == 0 ) + { + Com_Printf("CL_GlobalServers_f: Error: could not resolve address of%s master %s\n", + (a == 0 ? "" : a == 1 ? " alternate-1" : " alternate-2"), masteraddress); + continue; + } + else if ( i == 2 ) + { + to.port = BigShort(a == 0 ? PORT_MASTER : a == 1 ? ALT1PORT_MASTER : ALT2PORT_MASTER); + } + to.alternateProtocol = a; + + Com_Printf("Requesting servers from%s master %s...\n", + a == 0 ? "" : a == 1 ? " alternate-1" : " alternate-2", + masteraddress); + + cls.numglobalservers = -1; + cls.pingUpdateSource = AS_GLOBAL; + + Com_sprintf(command, sizeof(command), "getserversExt %s %i%s", + com_gamename->string, + a == 0 ? PROTOCOL_VERSION : a == 1 ? 70 : 69, + Cvar_VariableIntegerValue("net_enabled") & NET_ENABLEV4 ? "" : " ipv6"); + + for (i = 3; i < count; i++) + { + Q_strcat(command, sizeof(command), " "); + Q_strcat(command, sizeof(command), Cmd_Argv(i)); + } + + NET_OutOfBandPrint(NS_SERVER, to, "%s", command); + // outdent + } + CL_RequestMotd(); +} + +/* +================== +CL_GetPing +================== +*/ +void CL_GetPing(int n, char *buf, int buflen, int *pingtime) +{ + const char *str; + int time; + int maxPing; + + if (n < 0 || n >= MAX_PINGREQUESTS || !cl_pinglist[n].adr.port) + { + // empty or invalid slot + buf[0] = '\0'; + *pingtime = 0; + return; + } + + str = NET_AdrToStringwPort(cl_pinglist[n].adr); + Q_strncpyz(buf, str, buflen); + + time = cl_pinglist[n].time; + if (!time) + { + // check for timeout + time = Sys_Milliseconds() - cl_pinglist[n].start; + maxPing = Cvar_VariableIntegerValue("cl_maxPing"); + if (maxPing < 100) + { + maxPing = 100; + } + if (time < maxPing) + { + // not timed out yet + time = 0; + } + } + + CL_SetServerInfoByAddress(cl_pinglist[n].adr, cl_pinglist[n].info, cl_pinglist[n].time); + + *pingtime = time; +} + +/* +================== +CL_GetPingInfo +================== +*/ +void CL_GetPingInfo(int n, char *buf, int buflen) +{ + if (n < 0 || n >= MAX_PINGREQUESTS || !cl_pinglist[n].adr.port) + { + // empty or invalid slot + if (buflen) buf[0] = '\0'; + return; + } + + Q_strncpyz(buf, cl_pinglist[n].info, buflen); +} + +/* +================== +CL_ClearPing +================== +*/ +void CL_ClearPing(int n) +{ + if (n < 0 || n >= MAX_PINGREQUESTS) return; + + cl_pinglist[n].adr.port = 0; +} + +/* +================== +CL_GetPingQueueCount +================== +*/ +int CL_GetPingQueueCount(void) +{ + int i; + int count; + ping_t *pingptr; + + count = 0; + pingptr = cl_pinglist; + + for (i = 0; i < MAX_PINGREQUESTS; i++, pingptr++) + { + if (pingptr->adr.port) + { + count++; + } + } + + return (count); +} + +/* +================== +CL_GetFreePing +================== +*/ +static ping_t *CL_GetFreePing(void) +{ + ping_t *pingptr; + ping_t *best; + int oldest; + int i; + int time; + + pingptr = cl_pinglist; + for (i = 0; i < MAX_PINGREQUESTS; i++, pingptr++) + { + // find free ping slot + if (pingptr->adr.port) + { + if (!pingptr->time) + { + if (Sys_Milliseconds() - pingptr->start < 500) + { + // still waiting for response + continue; + } + } + else if (pingptr->time < 500) + { + // results have not been queried + continue; + } + } + + // clear it + pingptr->adr.port = 0; + return (pingptr); + } + + // use oldest entry + pingptr = cl_pinglist; + best = cl_pinglist; + oldest = INT_MIN; + for (i = 0; i < MAX_PINGREQUESTS; i++, pingptr++) + { + // scan for oldest + time = Sys_Milliseconds() - pingptr->start; + if (time > oldest) + { + oldest = time; + best = pingptr; + } + } + + return (best); +} + +/* +================== +CL_Ping_f +================== +*/ +static void CL_Ping_f(void) +{ + netadr_t to; + ping_t *pingptr; + const char *server; + int argc; + netadrtype_t family = NA_UNSPEC; + + argc = Cmd_Argc(); + + if (argc != 2 && argc != 3) + { + Com_Printf("usage: ping [-4|-6] server\n"); + return; + } + + if (argc == 2) + server = Cmd_Argv(1); + else + { + if (!strcmp(Cmd_Argv(1), "-4")) + family = NA_IP; + else if (!strcmp(Cmd_Argv(1), "-6")) + family = NA_IP6; + else + Com_Printf("warning: only -4 or -6 as address type understood.\n"); + + server = Cmd_Argv(2); + } + + ::memset(&to, 0, sizeof(netadr_t)); + + if (!NET_StringToAdr(server, &to, family)) + { + return; + } + + pingptr = CL_GetFreePing(); + + memcpy(&pingptr->adr, &to, sizeof(netadr_t)); + pingptr->start = Sys_Milliseconds(); + pingptr->time = 0; + + CL_SetServerInfoByAddress(pingptr->adr, NULL, 0); + + NET_OutOfBandPrint(NS_CLIENT, to, "getinfo xxx"); +} + +/* +================== +CL_UpdateVisiblePings_f +================== +*/ +bool CL_UpdateVisiblePings_f(int source) +{ + int slots, i; + char buff[MAX_STRING_CHARS]; + int pingTime; + int max; + bool status = false; + + if (source < 0 || source > AS_FAVORITES) + { + return false; + } + + cls.pingUpdateSource = source; + + slots = CL_GetPingQueueCount(); + if (slots < MAX_PINGREQUESTS) + { + serverInfo_t *server = NULL; + + switch (source) + { + case AS_LOCAL: + server = &cls.localServers[0]; + max = cls.numlocalservers; + break; + case AS_GLOBAL: + server = &cls.globalServers[0]; + max = cls.numglobalservers; + break; + case AS_FAVORITES: + server = &cls.favoriteServers[0]; + max = cls.numfavoriteservers; + break; + default: + return false; + } + for (i = 0; i < max; i++) + { + if (server[i].visible) + { + if (server[i].ping == -1) + { + int j; + + if (slots >= MAX_PINGREQUESTS) + { + break; + } + for (j = 0; j < MAX_PINGREQUESTS; j++) + { + if (!cl_pinglist[j].adr.port) + { + continue; + } + if (NET_CompareAdr(cl_pinglist[j].adr, server[i].adr)) + { + // already on the list + break; + } + } + if (j >= MAX_PINGREQUESTS) + { + status = true; + for (j = 0; j < MAX_PINGREQUESTS; j++) + { + if (!cl_pinglist[j].adr.port) + { + break; + } + } + memcpy(&cl_pinglist[j].adr, &server[i].adr, sizeof(netadr_t)); + cl_pinglist[j].start = Sys_Milliseconds(); + cl_pinglist[j].time = 0; + NET_OutOfBandPrint(NS_CLIENT, cl_pinglist[j].adr, "getinfo xxx"); + slots++; + } + } + // if the server has a ping higher than cl_maxPing or + // the ping packet got lost + else if (server[i].ping == 0) + { + // if we are updating global servers + if (source == AS_GLOBAL) + { + // + if (cls.numGlobalServerAddresses > 0) + { + // overwrite this server with one from the additional global servers + cls.numGlobalServerAddresses--; + CL_InitServerInfo(&server[i], &cls.globalServerAddresses[cls.numGlobalServerAddresses]); + // NOTE: the server[i].visible flag stays untouched + } + } + } + } + } + } + + if (slots) + { + status = true; + } + for (i = 0; i < MAX_PINGREQUESTS; i++) + { + if (!cl_pinglist[i].adr.port) + { + continue; + } + CL_GetPing(i, buff, MAX_STRING_CHARS, &pingTime); + if (pingTime != 0) + { + CL_ClearPing(i); + status = true; + } + } + + return status; +} + +/* +================== +CL_ServerStatus_f +================== +*/ +static void CL_ServerStatus_f(void) +{ + netadr_t to, *toptr = NULL; + const char *server; + serverStatus_t *serverStatus; + int argc; + netadrtype_t family = NA_UNSPEC; + + argc = Cmd_Argc(); + + if (argc != 2 && argc != 3) + { + if (clc.state != CA_ACTIVE || clc.demoplaying) + { + Com_Printf("Not connected to a server.\n"); + Com_Printf("usage: serverstatus [-4|-6] server\n"); + return; + } + + toptr = &clc.serverAddress; + } + + if (!toptr) + { + ::memset(&to, 0, sizeof(netadr_t)); + + if (argc == 2) + server = Cmd_Argv(1); + else + { + if (!strcmp(Cmd_Argv(1), "-4")) + family = NA_IP; + else if (!strcmp(Cmd_Argv(1), "-6")) + family = NA_IP6; + else + Com_Printf("warning: only -4 or -6 as address type understood.\n"); + + server = Cmd_Argv(2); + } + + toptr = &to; + if (!NET_StringToAdr(server, toptr, family)) return; + } + + NET_OutOfBandPrint(NS_CLIENT, *toptr, "getstatus"); + + serverStatus = CL_GetServerStatus(*toptr); + serverStatus->address = *toptr; + serverStatus->print = true; + serverStatus->pending = true; +} + +/* +============ +CL_InitRef +============ +*/ +static void CL_InitRef(void) +{ + refimport_t ri; + refexport_t *ret; +#ifdef USE_RENDERER_DLOPEN + GetRefAPI_t GetRefAPI; + char dllName[MAX_OSPATH]; +#endif + + Com_Printf("----- Initializing Renderer ----\n"); + +#ifdef USE_RENDERER_DLOPEN + cl_renderer = Cvar_Get("cl_renderer", "opengl2", CVAR_ARCHIVE | CVAR_LATCH); + + Com_sprintf(dllName, sizeof(dllName), "renderer_%s" DLL_EXT, cl_renderer->string); + + if (!(rendererLib = Sys_LoadDll(dllName, false)) && strcmp(cl_renderer->string, cl_renderer->resetString)) + { + Com_Printf("failed:\n\"%s\"\n", Sys_LibraryError()); + Cvar_ForceReset("cl_renderer"); + + Com_sprintf(dllName, sizeof(dllName), "renderer_opengl1" DLL_EXT); + rendererLib = Sys_LoadDll(dllName, false); + } + + if (!rendererLib) + { + Com_Printf("failed:\n\"%s\"\n", Sys_LibraryError()); + Com_Error(ERR_FATAL, "Failed to load renderer"); + } + + GetRefAPI = (GetRefAPI_t)Sys_LoadFunction(rendererLib, "GetRefAPI"); + if (!GetRefAPI) + { + Com_Error(ERR_FATAL, "Can't load symbol GetRefAPI: '%s'", Sys_LibraryError()); + } +#endif + + ri.Cmd_AddCommand = Cmd_AddCommand; + ri.Cmd_RemoveCommand = Cmd_RemoveCommand; + ri.Cmd_Argc = Cmd_Argc; + ri.Cmd_Argv = Cmd_Argv; + ri.Cmd_ExecuteText = Cbuf_ExecuteText; + ri.Printf = CL_RefPrintf; + ri.Error = Com_Error; + ri.Milliseconds = CL_ScaledMilliseconds; + ri.Malloc = CL_RefMalloc; + ri.Free = Z_Free; +#ifdef HUNK_DEBUG + ri.Hunk_AllocDebug = Hunk_AllocDebug; +#else + ri.Hunk_Alloc = Hunk_Alloc; +#endif + ri.Hunk_AllocateTempMemory = Hunk_AllocateTempMemory; + ri.Hunk_FreeTempMemory = Hunk_FreeTempMemory; + + ri.CM_ClusterPVS = CM_ClusterPVS; + ri.CM_DrawDebugSurface = CM_DrawDebugSurface; + + ri.FS_ReadFile = FS_ReadFile; + ri.FS_FreeFile = FS_FreeFile; + ri.FS_WriteFile = FS_WriteFile; + ri.FS_FreeFileList = FS_FreeFileList; + ri.FS_ListFiles = FS_ListFiles; + ri.FS_FileIsInPAK = FS_FileIsInPAK; + ri.FS_FileExists = FS_FileExists; + ri.Cvar_Get = Cvar_Get; + ri.Cvar_Set = Cvar_Set; + ri.Cvar_SetValue = Cvar_SetValue; + ri.Cvar_CheckRange = Cvar_CheckRange; + ri.Cvar_SetDescription = Cvar_SetDescription; + ri.Cvar_VariableIntegerValue = Cvar_VariableIntegerValue; + + // cinematic stuff + + ri.CIN_UploadCinematic = CIN_UploadCinematic; + ri.CIN_PlayCinematic = CIN_PlayCinematic; + ri.CIN_RunCinematic = CIN_RunCinematic; + + ri.CL_WriteAVIVideoFrame = CL_WriteAVIVideoFrame; + + ri.IN_Init = IN_Init; + ri.IN_Shutdown = IN_Shutdown; + ri.IN_Restart = IN_Restart; + + ri.Sys_GLimpSafeInit = Sys_GLimpSafeInit; + ri.Sys_GLimpInit = Sys_GLimpInit; + ri.Sys_LowPhysicalMemory = Sys_LowPhysicalMemory; + + ret = GetRefAPI(REF_API_VERSION, &ri); + +#if defined __USEA3D && defined __A3D_GEOM + hA3Dg_ExportRenderGeom(ret); +#endif + + Com_Printf("-------------------------------\n"); + + if (!ret) + { + Com_Error(ERR_FATAL, "Couldn't initialize refresh"); + } + + re = *ret; + + // unpause so the cgame definately gets a snapshot and renders a frame + Cvar_Set("cl_paused", "0"); +} + +/* +==================== +CL_ProtocolSpecificCommandsInit + +For adding/remove commands that depend on a/some +specific protocols, whenever the protcol may change +==================== +*/ +void CL_ProtocolSpecificCommandsInit(void) +{ + Con_MessageModesInit(); +} + +/* +==================== +CL_Init +==================== +*/ +void CL_Init(void) +{ + Com_Printf("----- Client Initialization -----\n"); + + Con_Init(); + + if (!com_fullyInitialized) + { + CL_ClearState(); + clc.state = CA_DISCONNECTED; // no longer CA_UNINITIALIZED + cl_oldGameSet = false; + } + + CL_InitInput(); + + // + // register our variables + // + cl_noprint = Cvar_Get("cl_noprint", "0", 0); + cl_motd = Cvar_Get("cl_motd", "1", 0); + + cl_timeout = Cvar_Get("cl_timeout", "200", 0); + + cl_timeNudge = Cvar_Get("cl_timeNudge", "0", CVAR_TEMP); + cl_shownet = Cvar_Get("cl_shownet", "0", CVAR_TEMP); + cl_showSend = Cvar_Get("cl_showSend", "0", CVAR_TEMP); + cl_showTimeDelta = Cvar_Get("cl_showTimeDelta", "0", CVAR_TEMP); + cl_freezeDemo = Cvar_Get("cl_freezeDemo", "0", CVAR_TEMP); + rcon_client_password = Cvar_Get("rconPassword", "", CVAR_TEMP); + cl_activeAction = Cvar_Get("activeAction", "", CVAR_TEMP); + + cl_timedemo = Cvar_Get("timedemo", "0", 0); + cl_timedemoLog = Cvar_Get("cl_timedemoLog", "", CVAR_ARCHIVE); + cl_autoRecordDemo = Cvar_Get("cl_autoRecordDemo", "0", CVAR_ARCHIVE); + cl_aviFrameRate = Cvar_Get("cl_aviFrameRate", "25", CVAR_ARCHIVE); + cl_aviMotionJpeg = Cvar_Get("cl_aviMotionJpeg", "1", CVAR_ARCHIVE); + cl_forceavidemo = Cvar_Get("cl_forceavidemo", "0", 0); + + rconAddress = Cvar_Get("rconAddress", "", 0); + + cl_yawspeed = Cvar_Get("cl_yawspeed", "140", CVAR_ARCHIVE); + cl_pitchspeed = Cvar_Get("cl_pitchspeed", "140", CVAR_ARCHIVE); + cl_anglespeedkey = Cvar_Get("cl_anglespeedkey", "1.5", 0); + + cl_maxpackets = Cvar_Get("cl_maxpackets", "30", CVAR_ARCHIVE); + cl_packetdup = Cvar_Get("cl_packetdup", "1", CVAR_ARCHIVE); + + cl_run = Cvar_Get("cl_run", "1", CVAR_ARCHIVE); + cl_sensitivity = Cvar_Get("sensitivity", "5", CVAR_ARCHIVE); + cl_mouseAccel = Cvar_Get("cl_mouseAccel", "0", CVAR_ARCHIVE); + cl_freelook = Cvar_Get("cl_freelook", "1", CVAR_ARCHIVE); + + // 0: legacy mouse acceleration + // 1: new implementation + cl_mouseAccelStyle = Cvar_Get("cl_mouseAccelStyle", "0", CVAR_ARCHIVE); + // offset for the power function (for style 1, ignored otherwise) + // this should be set to the max rate value + cl_mouseAccelOffset = Cvar_Get("cl_mouseAccelOffset", "5", CVAR_ARCHIVE); + Cvar_CheckRange(cl_mouseAccelOffset, 0.001f, 50000.0f, false); + + cl_showMouseRate = Cvar_Get("cl_showmouserate", "0", 0); + + cl_allowDownload = Cvar_Get("cl_allowDownload", "1", CVAR_ARCHIVE); + + if (cl_allowDownload->integer != -1) cl_allowDownload->integer = DLF_ENABLE; + + com_downloadPrompt = Cvar_Get("com_downloadPrompt", "0", CVAR_ROM); + Cvar_Get("com_downloadPromptText", "", CVAR_TEMP); + + cl_conXOffset = Cvar_Get("cl_conXOffset", "0", 0); +#ifdef __APPLE__ + // In game video is REALLY slow in Mac OS X right now due to driver slowness + cl_inGameVideo = Cvar_Get("r_inGameVideo", "0", CVAR_ARCHIVE); +#else + cl_inGameVideo = Cvar_Get("r_inGameVideo", "1", CVAR_ARCHIVE); +#endif + + cl_serverStatusResendTime = Cvar_Get("cl_serverStatusResendTime", "750", 0); + + m_pitch = Cvar_Get("m_pitch", "0.022", CVAR_ARCHIVE); + m_yaw = Cvar_Get("m_yaw", "0.022", CVAR_ARCHIVE); + m_forward = Cvar_Get("m_forward", "0.25", CVAR_ARCHIVE); + m_side = Cvar_Get("m_side", "0.25", CVAR_ARCHIVE); +#ifdef __APPLE__ + // Input is jittery on OS X w/o this + m_filter = Cvar_Get("m_filter", "1", CVAR_ARCHIVE); +#else + m_filter = Cvar_Get("m_filter", "0", CVAR_ARCHIVE); +#endif + + j_pitch = Cvar_Get("j_pitch", "0.022", CVAR_ARCHIVE); + j_yaw = Cvar_Get("j_yaw", "-0.022", CVAR_ARCHIVE); + j_forward = Cvar_Get("j_forward", "-0.25", CVAR_ARCHIVE); + j_side = Cvar_Get("j_side", "0.25", CVAR_ARCHIVE); + j_up = Cvar_Get("j_up", "0", CVAR_ARCHIVE); + + j_pitch_axis = Cvar_Get("j_pitch_axis", "3", CVAR_ARCHIVE); + j_yaw_axis = Cvar_Get("j_yaw_axis", "2", CVAR_ARCHIVE); + j_forward_axis = Cvar_Get("j_forward_axis", "1", CVAR_ARCHIVE); + j_side_axis = Cvar_Get("j_side_axis", "0", CVAR_ARCHIVE); + j_up_axis = Cvar_Get("j_up_axis", "4", CVAR_ARCHIVE); + + Cvar_CheckRange(j_pitch_axis, 0, MAX_JOYSTICK_AXIS - 1, true); + Cvar_CheckRange(j_yaw_axis, 0, MAX_JOYSTICK_AXIS - 1, true); + Cvar_CheckRange(j_forward_axis, 0, MAX_JOYSTICK_AXIS - 1, true); + Cvar_CheckRange(j_side_axis, 0, MAX_JOYSTICK_AXIS - 1, true); + Cvar_CheckRange(j_up_axis, 0, MAX_JOYSTICK_AXIS - 1, true); + + cl_motdString = Cvar_Get("cl_motdString", "", CVAR_ROM); + + Cvar_Get("cl_maxPing", "800", CVAR_ARCHIVE); + + cl_lanForcePackets = Cvar_Get("cl_lanForcePackets", "1", CVAR_ARCHIVE); + + cl_guidServerUniq = Cvar_Get("cl_guidServerUniq", "1", CVAR_ARCHIVE); + + cl_rsaAuth = Cvar_Get("cl_rsaAuth", "0", CVAR_INIT | CVAR_PROTECTED); + + // ~ and `, as keys and characters + cl_consoleKeys = Cvar_Get("cl_consoleKeys", "~ ` 0x7e 0x60", CVAR_ARCHIVE); + + cl_clantag = Cvar_Get ("cl_clantag", "", CVAR_ARCHIVE); + + // userinfo + Cvar_Get("name", "UnnamedPlayer", CVAR_USERINFO | CVAR_ARCHIVE); + cl_rate = Cvar_Get("rate", "25000", CVAR_USERINFO | CVAR_ARCHIVE); + Cvar_Get("snaps", "40", CVAR_USERINFO | CVAR_ARCHIVE); + Cvar_Get("color1", "4", CVAR_USERINFO | CVAR_ARCHIVE); + Cvar_Get("color2", "5", CVAR_USERINFO | CVAR_ARCHIVE); + Cvar_Get("handicap", "100", CVAR_USERINFO | CVAR_ARCHIVE); + Cvar_Get("sex", "male", CVAR_USERINFO | CVAR_ARCHIVE); + + Cvar_Get("password", "", CVAR_USERINFO); + +#ifdef USE_MUMBLE + cl_useMumble = Cvar_Get("cl_useMumble", "0", CVAR_ARCHIVE | CVAR_LATCH); + cl_mumbleScale = Cvar_Get("cl_mumbleScale", "0.0254", CVAR_ARCHIVE); +#endif + +#ifdef USE_VOIP + cl_voipSend = Cvar_Get("cl_voipSend", "0", 0); + cl_voipSendTarget = Cvar_Get("cl_voipSendTarget", "spatial", 0); + cl_voipGainDuringCapture = Cvar_Get("cl_voipGainDuringCapture", "0.2", CVAR_ARCHIVE); + cl_voipCaptureMult = Cvar_Get("cl_voipCaptureMult", "2.0", CVAR_ARCHIVE); + cl_voipUseVAD = Cvar_Get("cl_voipUseVAD", "0", CVAR_ARCHIVE); + cl_voipVADThreshold = Cvar_Get("cl_voipVADThreshold", "0.25", CVAR_ARCHIVE); + cl_voipShowMeter = Cvar_Get("cl_voipShowMeter", "1", CVAR_ARCHIVE); + + cl_voip = Cvar_Get("cl_voip", "1", CVAR_ARCHIVE); + Cvar_CheckRange(cl_voip, 0, 1, true); + cl_voipProtocol = Cvar_Get("cl_voipProtocol", cl_voip->integer ? "opus" : "", CVAR_USERINFO | CVAR_ROM); +#endif + + // cgame might not be initialized before menu is used + Cvar_Get("cg_viewsize", "100", CVAR_ARCHIVE); + // Make sure cg_stereoSeparation is zero as that variable is deprecated and should not be used anymore. + Cvar_Get("cg_stereoSeparation", "0", CVAR_ROM); + + // + // register our commands + // + Cmd_AddCommand("cmd", CL_ForwardToServer_f); + Cmd_AddCommand("configstrings", CL_Configstrings_f); + Cmd_AddCommand("clientinfo", CL_Clientinfo_f); + Cmd_AddCommand("snd_restart", CL_Snd_Restart_f); + Cmd_AddCommand("vid_restart", CL_Vid_Restart_f); + Cmd_AddCommand("disconnect", CL_Disconnect_f); + Cmd_AddCommand("record", CL_Record_f); + Cmd_AddCommand("demo", CL_PlayDemo_f); + Cmd_SetCommandCompletionFunc("demo", CL_CompleteDemoName); + Cmd_AddCommand("cinematic", CL_PlayCinematic_f); + Cmd_AddCommand("stoprecord", CL_StopRecord_f); + Cmd_AddCommand("connect", CL_Connect_f); + Cmd_AddCommand("reconnect", CL_Reconnect_f); + Cmd_AddCommand("localservers", CL_LocalServers_f); + Cmd_AddCommand("globalservers", CL_GlobalServers_f); + Cmd_AddCommand("rcon", CL_Rcon_f); + Cmd_SetCommandCompletionFunc("rcon", CL_CompleteRcon); + Cmd_AddCommand("ping", CL_Ping_f); + Cmd_AddCommand("serverstatus", CL_ServerStatus_f); + Cmd_AddCommand("showip", CL_ShowIP_f); + Cmd_AddCommand("fs_openedList", CL_OpenedPK3List_f); + Cmd_AddCommand("fs_referencedList", CL_ReferencedPK3List_f); + Cmd_AddCommand("model", CL_SetModel_f); + Cmd_AddCommand("video", CL_Video_f); + Cmd_AddCommand("stopvideo", CL_StopVideo_f); + Cmd_AddCommand("downloadUpdate", CL_DownloadUpdate_f); + Cmd_AddCommand("installUpdate", CL_InstallUpdate_f); + Cmd_AddCommand("checkForUpdate", CL_CheckForUpdate_f); + Cmd_AddCommand("browseHomepath", CL_BrowseHomepath_f); + Cmd_AddCommand("browseDemos", CL_BrowseDemos_f); + Cmd_AddCommand("browseScreenShots", CL_BrowseScreenShots_f); + + CL_InitRef(); + + SCR_Init(); + + // Cbuf_Execute (); + + Cvar_Set("cl_running", "1"); + + if (cl_rsaAuth->integer) CL_LoadRSAKeypair(); + + CL_GenerateQKey(); + Cvar_Get("cl_guid", "", CVAR_USERINFO | CVAR_ROM); + if (clc.state == CA_DISCONNECTED) CL_UpdateGUID(NULL, 0); + + Com_Printf("----- Client Initialization Complete -----\n"); +} + +/* +=============== +CL_Shutdown + +=============== +*/ +void CL_Shutdown(const char *finalmsg, bool disconnect, bool quit) +{ + static bool recursive = false; + int realtime; + + // check whether the client is running at all. + if (!(com_cl_running && com_cl_running->integer)) return; + + Com_Printf("----- Client Shutdown (%s) -----\n", finalmsg); + + if (recursive) + { + Com_Printf("WARNING: Recursive shutdown\n"); + return; + } + recursive = true; + + noGameRestart = quit; + + if (disconnect) CL_Disconnect(true); + + CL_ClearMemory(true); + CL_Snd_Shutdown(); + + Cmd_RemoveCommand("cmd"); + Cmd_RemoveCommand("configstrings"); + Cmd_RemoveCommand("clientinfo"); + Cmd_RemoveCommand("snd_restart"); + Cmd_RemoveCommand("vid_restart"); + Cmd_RemoveCommand("disconnect"); + Cmd_RemoveCommand("record"); + Cmd_RemoveCommand("demo"); + Cmd_RemoveCommand("cinematic"); + Cmd_RemoveCommand("stoprecord"); + Cmd_RemoveCommand("connect"); + Cmd_RemoveCommand("reconnect"); + Cmd_RemoveCommand("localservers"); + Cmd_RemoveCommand("globalservers"); + Cmd_RemoveCommand("rcon"); + Cmd_RemoveCommand("ping"); + Cmd_RemoveCommand("serverstatus"); + Cmd_RemoveCommand("showip"); + Cmd_RemoveCommand("fs_openedList"); + Cmd_RemoveCommand("fs_referencedList"); + Cmd_RemoveCommand("model"); + Cmd_RemoveCommand("video"); + Cmd_RemoveCommand("stopvideo"); + + CL_ShutdownInput(); + Con_Shutdown(); + + Cvar_Set("cl_running", "0"); + + recursive = false; + + if (cl_rsaAuth->integer) CL_UnloadRSAKeypair(); + + realtime = cls.realtime; + ::memset(&cls, 0, sizeof(cls)); + cls.realtime = realtime; + Key_SetCatcher(0); + + Com_Printf("-----------------------\n"); +} diff --git a/src/client/cl_net_chan.cpp b/src/client/cl_net_chan.cpp new file mode 100644 index 0000000..80ac25d --- /dev/null +++ b/src/client/cl_net_chan.cpp @@ -0,0 +1,190 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#include "client.h" + +#include "qcommon/q_shared.h" +#include "qcommon/qcommon.h" + +/* +============== +CL_Netchan_Encode + + // first 12 bytes of the data are always: + long serverId; + long messageAcknowledge; + long reliableAcknowledge; + +============== +*/ +static void CL_Netchan_Encode(msg_t *msg) +{ + int serverId, messageAcknowledge, reliableAcknowledge; + int i, idx, srdc, sbit; + bool soob; + byte key, *string; + + if (msg->cursize <= CL_ENCODE_START) + { + return; + } + + srdc = msg->readcount; + sbit = msg->bit; + soob = msg->oob; + + msg->bit = 0; + msg->readcount = 0; + msg->oob = false; + + serverId = MSG_ReadLong(msg); + messageAcknowledge = MSG_ReadLong(msg); + reliableAcknowledge = MSG_ReadLong(msg); + + msg->oob = soob; + msg->bit = sbit; + msg->readcount = srdc; + + string = (byte *)clc.serverCommands[reliableAcknowledge & (MAX_RELIABLE_COMMANDS - 1)]; + idx = 0; + // + key = clc.challenge ^ serverId ^ messageAcknowledge; + for (i = CL_ENCODE_START; i < msg->cursize; i++) + { + // modify the key with the last received now acknowledged server command + if (!string[idx]) idx = 0; + if (string[idx] > 127 || (string[idx] == '%' && clc.netchan.alternateProtocol == 2)) + { + key ^= '.' << (i & 1); + } + else + { + key ^= string[idx] << (i & 1); + } + idx++; + // encode the data with this key + *(msg->data + i) = (*(msg->data + i)) ^ key; + } +} + +/* +============== +CL_Netchan_Decode + + // first four bytes of the data are always: + long reliableAcknowledge; + +============== +*/ +static void CL_Netchan_Decode(msg_t *msg) +{ + long reliableAcknowledge, i, idx; + byte key, *string; + int srdc, sbit; + bool soob; + + srdc = msg->readcount; + sbit = msg->bit; + soob = msg->oob; + + msg->oob = false; + + reliableAcknowledge = MSG_ReadLong(msg); + + msg->oob = soob; + msg->bit = sbit; + msg->readcount = srdc; + + string = (byte *)clc.reliableCommands[reliableAcknowledge & (MAX_RELIABLE_COMMANDS - 1)]; + idx = 0; + // xor the client challenge with the netchan sequence number (need something that changes every message) + key = clc.challenge ^ LittleLong(*(unsigned *)msg->data); + for (i = msg->readcount + CL_DECODE_START; i < msg->cursize; i++) + { + // modify the key with the last sent and with this message acknowledged client command + if (!string[idx]) idx = 0; + if (string[idx] > 127 || (string[idx] == '%' && clc.netchan.alternateProtocol == 2)) + { + key ^= '.' << (i & 1); + } + else + { + key ^= string[idx] << (i & 1); + } + idx++; + // decode the data with this key + *(msg->data + i) = *(msg->data + i) ^ key; + } +} + +/* +================= +CL_Netchan_TransmitNextFragment +================= +*/ +static bool CL_Netchan_TransmitNextFragment(netchan_t *chan) +{ + if (chan->unsentFragments) + { + Netchan_TransmitNextFragment(chan); + return true; + } + + return false; +} + +/* +=============== +CL_Netchan_Transmit +================ +*/ +void CL_Netchan_Transmit(netchan_t *chan, msg_t *msg) +{ + MSG_WriteByte(msg, clc_EOF); + + if (chan->alternateProtocol != 0) CL_Netchan_Encode(msg); + Netchan_Transmit(chan, msg->cursize, msg->data); + + // Transmit all fragments without delay + while (CL_Netchan_TransmitNextFragment(chan)) + { + Com_DPrintf("WARNING: #462 unsent fragments (not supposed to happen!)\n"); + } +} + +/* +================= +CL_Netchan_Process +================= +*/ +bool CL_Netchan_Process(netchan_t *chan, msg_t *msg) +{ + int ret; + + ret = Netchan_Process(chan, msg); + if (!ret) return false; + if (chan->alternateProtocol != 0) CL_Netchan_Decode(msg); + + return true; +} diff --git a/src/client/cl_parse.cpp b/src/client/cl_parse.cpp new file mode 100644 index 0000000..111211a --- /dev/null +++ b/src/client/cl_parse.cpp @@ -0,0 +1,961 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ +// cl_parse.c -- parse a message received from the server + +#include "client.h" + +const char *svc_strings[256] = { + "svc_bad", + "svc_nop", + "svc_gamestate", + "svc_configstring", + "svc_baseline", + "svc_serverCommand", + "svc_download", + "svc_snapshot", + "svc_EOF", + "svc_voipSpeex", + "svc_voipOpus", +}; + +void SHOWNET( msg_t *msg, const char *s) { + if ( cl_shownet->integer >= 2) { + Com_Printf ("%3i:%s\n", msg->readcount-1, s); + } +} + + +/* +========================================================================= + +MESSAGE PARSING + +========================================================================= +*/ + +/* +================== +CL_DeltaEntity + +Parses deltas from the given base and adds the resulting entity +to the current frame +================== +*/ +static void CL_DeltaEntity(msg_t *msg, clSnapshot_t *frame, int newnum, entityState_t *old, bool unchanged) +{ + entityState_t *state; + + // save the parsed entity state into the big circular buffer so it can be + // used as the source for a later delta + state = &cl.parseEntities[cl.parseEntitiesNum & (MAX_PARSE_ENTITIES-1)]; + + if ( unchanged ) + { + *state = *old; + } + else + { + MSG_ReadDeltaEntity( clc.netchan.alternateProtocol, msg, old, state, newnum ); + } + + if ( state->number == (MAX_GENTITIES-1) ) + { + return; // entity was delta removed + } + + cl.parseEntitiesNum++; + frame->numEntities++; +} + +/* +================== +CL_ParsePacketEntities + +================== +*/ +static void CL_ParsePacketEntities( msg_t *msg, clSnapshot_t *oldframe, clSnapshot_t *newframe) +{ + int newnum; + entityState_t *oldstate; + int oldindex, oldnum; + + newframe->parseEntitiesNum = cl.parseEntitiesNum; + newframe->numEntities = 0; + + // delta from the entities present in oldframe + oldindex = 0; + oldstate = NULL; + if (!oldframe) { + oldnum = 99999; + } else { + if ( oldindex >= oldframe->numEntities ) { + oldnum = 99999; + } else { + oldstate = &cl.parseEntities[ + (oldframe->parseEntitiesNum + oldindex) & (MAX_PARSE_ENTITIES-1)]; + oldnum = oldstate->number; + } + } + + while ( 1 ) { + // read the entity index number + newnum = MSG_ReadBits( msg, GENTITYNUM_BITS ); + + if ( newnum == (MAX_GENTITIES-1) ) { + break; + } + + if ( msg->readcount > msg->cursize ) { + Com_Error (ERR_DROP,"CL_ParsePacketEntities: end of message"); + } + + while ( oldnum < newnum ) { + // one or more entities from the old packet are unchanged + if ( cl_shownet->integer == 3 ) { + Com_Printf ("%3i: unchanged: %i\n", msg->readcount, oldnum); + } + CL_DeltaEntity( msg, newframe, oldnum, oldstate, true ); + + oldindex++; + + if ( oldindex >= oldframe->numEntities ) { + oldnum = 99999; + } else { + oldstate = &cl.parseEntities[ + (oldframe->parseEntitiesNum + oldindex) & (MAX_PARSE_ENTITIES-1)]; + oldnum = oldstate->number; + } + } + if (oldnum == newnum) { + // delta from previous state + if ( cl_shownet->integer == 3 ) { + Com_Printf ("%3i: delta: %i\n", msg->readcount, newnum); + } + CL_DeltaEntity( msg, newframe, newnum, oldstate, false ); + + oldindex++; + + if ( oldindex >= oldframe->numEntities ) { + oldnum = 99999; + } else { + oldstate = &cl.parseEntities[ + (oldframe->parseEntitiesNum + oldindex) & (MAX_PARSE_ENTITIES-1)]; + oldnum = oldstate->number; + } + continue; + } + + if ( oldnum > newnum ) { + // delta from baseline + if ( cl_shownet->integer == 3 ) { + Com_Printf ("%3i: baseline: %i\n", msg->readcount, newnum); + } + CL_DeltaEntity( msg, newframe, newnum, &cl.entityBaselines[newnum], false ); + continue; + } + + } + + // any remaining entities in the old frame are copied over + while ( oldnum != 99999 ) { + // one or more entities from the old packet are unchanged + if ( cl_shownet->integer == 3 ) { + Com_Printf ("%3i: unchanged: %i\n", msg->readcount, oldnum); + } + CL_DeltaEntity( msg, newframe, oldnum, oldstate, true ); + + oldindex++; + + if ( oldindex >= oldframe->numEntities ) { + oldnum = 99999; + } else { + oldstate = &cl.parseEntities[ + (oldframe->parseEntitiesNum + oldindex) & (MAX_PARSE_ENTITIES-1)]; + oldnum = oldstate->number; + } + } +} + + +/* +================ +CL_ParseSnapshot + +If the snapshot is parsed properly, it will be copied to +cl.snap and saved in cl.snapshots[]. If the snapshot is invalid +for any reason, no changes to the state will be made at all. +================ +*/ +void CL_ParseSnapshot( msg_t *msg ) { + int len; + clSnapshot_t *old; + clSnapshot_t newSnap; + int deltaNum; + int oldMessageNum; + int i, packetNum; + + // get the reliable sequence acknowledge number + // NOTE: now sent with all server to client messages + //clc.reliableAcknowledge = MSG_ReadLong( msg ); + + // read in the new snapshot to a temporary buffer + // we will only copy to cl.snap if it is valid + ::memset(&newSnap, 0, sizeof(newSnap)); + + // we will have read any new server commands in this + // message before we got to svc_snapshot + newSnap.serverCommandNum = clc.serverCommandSequence; + + newSnap.serverTime = MSG_ReadLong( msg ); + + // if we were just unpaused, we can only *now* really let the + // change come into effect or the client hangs. + cl_paused->modified = false; + + newSnap.messageNum = clc.serverMessageSequence; + + deltaNum = MSG_ReadByte( msg ); + if ( !deltaNum ) { + newSnap.deltaNum = -1; + } else { + newSnap.deltaNum = newSnap.messageNum - deltaNum; + } + newSnap.snapFlags = MSG_ReadByte( msg ); + + // If the frame is delta compressed from data that we + // no longer have available, we must suck up the rest of + // the frame, but not use it, then ask for a non-compressed + // message + if ( newSnap.deltaNum <= 0 ) { + newSnap.valid = true; // uncompressed frame + old = NULL; + clc.demowaiting = false; // we can start recording now + } else { + old = &cl.snapshots[newSnap.deltaNum & PACKET_MASK]; + if ( !old->valid ) { + // should never happen + Com_DPrintf ("Delta from invalid frame (not supposed to happen!).\n"); + } else if ( old->messageNum != newSnap.deltaNum ) { + // The frame that the server did the delta from + // is too old, so we can't reconstruct it properly. + Com_DPrintf ("Delta frame too old.\n"); + } else if ( cl.parseEntitiesNum - old->parseEntitiesNum > MAX_PARSE_ENTITIES - MAX_SNAPSHOT_ENTITIES ) { + Com_DPrintf ("Delta parseEntitiesNum too old.\n"); + } else { + newSnap.valid = true; // valid delta parse + } + } + + // read areamask + len = MSG_ReadByte( msg ); + + if(len > sizeof(newSnap.areamask)) + { + Com_Error (ERR_DROP,"CL_ParseSnapshot: Invalid size %d for areamask", len); + return; + } + + MSG_ReadData( msg, &newSnap.areamask, len); + + // read playerinfo + SHOWNET( msg, "playerstate" ); + if ( clc.netchan.alternateProtocol == 2 ) { + if ( old ) { + MSG_ReadDeltaAlternatePlayerstate( msg, &old->alternatePs, &newSnap.alternatePs ); + } else { + MSG_ReadDeltaAlternatePlayerstate( msg, NULL, &newSnap.alternatePs ); + } + } else { + if ( old ) { + MSG_ReadDeltaPlayerstate( msg, &old->ps, &newSnap.ps ); + } else { + MSG_ReadDeltaPlayerstate( msg, NULL, &newSnap.ps ); + } + } + + // read packet entities + SHOWNET( msg, "packet entities" ); + CL_ParsePacketEntities( msg, old, &newSnap ); + + // if not valid, dump the entire thing now that it has + // been properly read + if ( !newSnap.valid ) { + return; + } + + // clear the valid flags of any snapshots between the last + // received and this one, so if there was a dropped packet + // it won't look like something valid to delta from next + // time we wrap around in the buffer + oldMessageNum = cl.snap.messageNum + 1; + + if ( newSnap.messageNum - oldMessageNum >= PACKET_BACKUP ) { + oldMessageNum = newSnap.messageNum - ( PACKET_BACKUP - 1 ); + } + for ( ; oldMessageNum < newSnap.messageNum ; oldMessageNum++ ) { + cl.snapshots[oldMessageNum & PACKET_MASK].valid = false; + } + + // copy to the current good spot + cl.snap = newSnap; + cl.snap.ping = 999; + // calculate ping time + for ( i = 0 ; i < PACKET_BACKUP ; i++ ) { + packetNum = ( clc.netchan.outgoingSequence - 1 - i ) & PACKET_MASK; + if ( ( clc.netchan.alternateProtocol == 2 ? cl.snap.alternatePs.commandTime : cl.snap.ps.commandTime ) >= cl.outPackets[ packetNum ].p_serverTime ) { + cl.snap.ping = cls.realtime - cl.outPackets[ packetNum ].p_realtime; + break; + } + } + // save the frame off in the backup array for later delta comparisons + cl.snapshots[cl.snap.messageNum & PACKET_MASK] = cl.snap; + + if (cl_shownet->integer == 3) { + Com_Printf( " snapshot:%i delta:%i ping:%i\n", cl.snap.messageNum, + cl.snap.deltaNum, cl.snap.ping ); + } + + cl.newSnapshots = true; +} + + +//===================================================================== + +bool cl_connectedToPureServer; +bool cl_connectedToCheatServer; + +/* +================== +CL_SystemInfoChanged + +The systeminfo configstring has been changed, so parse +new information out of it. This will happen at every +gamestate, and possibly during gameplay. +================== +*/ +void CL_SystemInfoChanged( void ) { + char *systemInfo; + const char *s, *t; + char key[BIG_INFO_KEY]; + char value[BIG_INFO_VALUE]; + bool gameSet; + + systemInfo = cl.gameState.stringData + cl.gameState.stringOffsets[ CS_SYSTEMINFO ]; + // NOTE TTimo: + // when the serverId changes, any further messages we send to the server will use this new serverId + // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=475 + // in some cases, outdated cp commands might get sent with this news serverId + cl.serverId = atoi( Info_ValueForKey( systemInfo, "sv_serverid" ) ); + +#ifdef USE_VOIP + s = Info_ValueForKey( systemInfo, "sv_voipProtocol" ); + clc.voipEnabled = !Q_stricmp(s, "opus"); +#endif + + // only set the paks, fs_game, and sv_pure when playing a demo + if ( !clc.demoplaying ) { + s = Info_ValueForKey( systemInfo, "sv_cheats" ); + cl_connectedToCheatServer = (bool)atoi( s ); + if ( !cl_connectedToCheatServer ) { + Cvar_SetCheatState(); + } + } + + // check pure server string + s = Info_ValueForKey( systemInfo, "sv_paks" ); + t = Info_ValueForKey( systemInfo, "sv_pakNames" ); + FS_PureServerSetLoadedPaks( s, t ); + + s = Info_ValueForKey( systemInfo, "sv_referencedPaks" ); + t = Info_ValueForKey( systemInfo, "sv_referencedPakNames" ); + FS_PureServerSetReferencedPaks( s, t ); + + gameSet = false; + // scan through all the variables in the systeminfo and locally set cvars to match + s = systemInfo; + while ( s ) { + int cvar_flags; + + Info_NextPair( &s, key, value ); + if ( !key[0] ) { + break; + } + + // ehw! + if (!Q_stricmp(key, "fs_game")) + { + if(FS_CheckDirTraversal(value)) + { + Com_Printf(S_COLOR_YELLOW "WARNING: Server sent invalid fs_game value %s\n", value); + continue; + } + + gameSet = true; + } else if ( clc.demoplaying && Q_stricmp(key, "sv_pure" ) ) { + continue; + } + + if((cvar_flags = Cvar_Flags(key)) == CVAR_NONEXISTENT) + Cvar_Get(key, value, CVAR_SERVER_CREATED | CVAR_ROM); + else + { + // If this cvar may not be modified by a server discard the value. + if(!(cvar_flags & (CVAR_SYSTEMINFO | CVAR_SERVER_CREATED | CVAR_USER_CREATED))) + { + if(Q_stricmp(key, "g_synchronousClients") && Q_stricmp(key, "pmove_fixed") && + Q_stricmp(key, "pmove_msec")) + { + Com_Printf(S_COLOR_YELLOW "WARNING: server is not allowed to set %s=%s\n", key, value); + continue; + } + } + + Cvar_SetSafe(key, value); + } + } + // if game folder should not be set and it is set at the client side + if ( !gameSet && *Cvar_VariableString("fs_game") ) { + Cvar_Set( "fs_game", "" ); + } + cl_connectedToPureServer = (bool)Cvar_VariableValue( "sv_pure" ); +} + +/* +================== +CL_ParseServerInfo +================== +*/ +static void CL_ParseServerInfo(void) +{ + const char *serverInfo; + + serverInfo = cl.gameState.stringData + + cl.gameState.stringOffsets[ CS_SERVERINFO ]; + + clc.sv_allowDownload = atoi(Info_ValueForKey(serverInfo, + "sv_allowDownload")); + Q_strncpyz(clc.sv_dlURL, + Info_ValueForKey(serverInfo, "sv_dlURL"), + sizeof(clc.sv_dlURL)); +} + +/* +================== +CL_ParseGamestate +================== +*/ +void CL_ParseGamestate( msg_t *msg ) { + int i; + entityState_t *es; + int newnum; + entityState_t nullstate; + int cmd; + char *s; + char oldGame[MAX_QPATH]; + + clc.connectPacketCount = 0; + + // wipe local client state + CL_ClearState(); + + // a gamestate always marks a server command sequence + clc.serverCommandSequence = MSG_ReadLong( msg ); + + // parse all the configstrings and baselines + cl.gameState.dataCount = 1; // leave a 0 at the beginning for uninitialized configstrings + while ( 1 ) { + cmd = MSG_ReadByte( msg ); + + if ( cmd == svc_EOF ) { + break; + } + + if ( cmd == svc_configstring ) { + int len; + + i = MSG_ReadShort( msg ); + if ( i < 0 || i >= MAX_CONFIGSTRINGS ) { + Com_Error( ERR_DROP, "configstring > MAX_CONFIGSTRINGS" ); + } + s = MSG_ReadBigString( msg ); + len = strlen( s ); + + if ( len + 1 + cl.gameState.dataCount > MAX_GAMESTATE_CHARS ) { + Com_Error( ERR_DROP, "MAX_GAMESTATE_CHARS exceeded" ); + } + + // append it to the gameState string buffer + cl.gameState.stringOffsets[ i ] = cl.gameState.dataCount; + ::memcpy( cl.gameState.stringData + cl.gameState.dataCount, s, len + 1 ); + cl.gameState.dataCount += len + 1; + } else if ( cmd == svc_baseline ) { + newnum = MSG_ReadBits( msg, GENTITYNUM_BITS ); + if ( newnum < 0 || newnum >= MAX_GENTITIES ) { + Com_Error( ERR_DROP, "Baseline number out of range: %i", newnum ); + } + ::memset(&nullstate, 0, sizeof(nullstate)); + es = &cl.entityBaselines[ newnum ]; + MSG_ReadDeltaEntity( clc.netchan.alternateProtocol, msg, &nullstate, es, newnum ); + } else { + Com_Error( ERR_DROP, "CL_ParseGamestate: bad command byte" ); + } + } + + clc.clientNum = MSG_ReadLong(msg); + // read the checksum feed + clc.checksumFeed = MSG_ReadLong( msg ); + + // save old gamedir + Cvar_VariableStringBuffer("fs_game", oldGame, sizeof(oldGame)); + + // parse useful values out of CS_SERVERINFO + CL_ParseServerInfo(); + + // parse serverId and other cvars + CL_SystemInfoChanged(); + + // stop recording now so the demo won't have an unnecessary level load at the end. + if(cl_autoRecordDemo->integer && clc.demorecording) + CL_StopRecord_f(); + + // reinitialize the filesystem if the game directory has changed + if(!cl_oldGameSet && (Cvar_Flags("fs_game") & CVAR_MODIFIED)) + { + cl_oldGameSet = true; + Q_strncpyz(cl_oldGame, oldGame, sizeof(cl_oldGame)); + } + + FS_ConditionalRestart(clc.checksumFeed, false); + + // This used to call CL_StartHunkUsers, but now we enter the download state before loading the + // cgame + CL_InitDownloads(); + + // make sure the game starts + Cvar_Set( "cl_paused", "0" ); +} + + +//===================================================================== + +/* +===================== +CL_ParseDownload + +A download message has been received from the server +===================== +*/ +void CL_ParseDownload ( msg_t *msg ) { + int size; + unsigned char data[MAX_MSGLEN]; + uint16_t block; + + if (!*clc.downloadTempName) { + Com_Printf("Server sending download, but no download was requested\n"); + CL_AddReliableCommand("stopdl", false); + return; + } + + // read the data + block = MSG_ReadShort ( msg ); + + if(!block && !clc.downloadBlock) + { + // block zero is special, contains file size + clc.downloadSize = MSG_ReadLong ( msg ); + + Cvar_SetValue( "cl_downloadSize", clc.downloadSize ); + + if (clc.downloadSize < 0) + { + Com_Error( ERR_DROP, "%s", MSG_ReadString( msg ) ); + return; + } + } + + size = MSG_ReadShort ( msg ); + if (size < 0 || size > sizeof(data)) + { + Com_Error(ERR_DROP, "CL_ParseDownload: Invalid size %d for download chunk", size); + return; + } + + MSG_ReadData(msg, data, size); + + if((clc.downloadBlock & 0xFFFF) != block) + { + Com_DPrintf( "CL_ParseDownload: Expected block %d, got %d\n", (clc.downloadBlock & 0xFFFF), block); + return; + } + + // open the file if not opened yet + if (!clc.download) + { + clc.download = FS_SV_FOpenFileWrite( clc.downloadTempName ); + + if (!clc.download) { + Com_Printf( "Could not create %s\n", clc.downloadTempName ); + CL_AddReliableCommand("stopdl", false); + CL_NextDownload(); + return; + } + } + + if (size) + FS_Write( data, size, clc.download ); + + CL_AddReliableCommand(va("nextdl %d", clc.downloadBlock), false); + clc.downloadBlock++; + + clc.downloadCount += size; + + // So UI gets access to it + Cvar_SetValue( "cl_downloadCount", clc.downloadCount ); + + if (!size) { // A zero length block means EOF + if (clc.download) { + FS_FCloseFile( clc.download ); + clc.download = 0; + + // rename the file + FS_SV_Rename( clc.downloadTempName, clc.downloadName, false ); + } + + // send intentions now + // We need this because without it, we would hold the last nextdl and then start + // loading right away. If we take a while to load, the server is happily trying + // to send us that last block over and over. + // Write it twice to help make sure we acknowledge the download + CL_WritePacket(); + CL_WritePacket(); + + // get another file if needed + CL_NextDownload (); + } +} + +#ifdef USE_VOIP +static bool CL_ShouldIgnoreVoipSender(int sender) +{ + if (!cl_voip->integer) + return true; // VoIP is disabled. + else if ((sender == clc.clientNum) && (!clc.demoplaying)) + return true; // ignore own voice (unless playing back a demo). + else if (clc.voipMuteAll) + return true; // all channels are muted with extreme prejudice. + else if (clc.voipIgnore[sender]) + return true; // just ignoring this guy. + else if (clc.voipGain[sender] == 0.0f) + return true; // too quiet to play. + + return false; +} + +/* +===================== +CL_PlayVoip + +Play raw data +===================== +*/ + +static void CL_PlayVoip(int sender, int samplecnt, const byte *data, int flags) +{ + if(flags & VOIP_DIRECT) + { + S_RawSamples(sender + 1, samplecnt, 48000, 2, 1, data, clc.voipGain[sender], -1); + } + + if(flags & VOIP_SPATIAL) + { + S_RawSamples(sender + MAX_CLIENTS + 1, samplecnt, 48000, 2, 1, data, 1.0f, sender); + } +} + +/* +===================== +CL_ParseVoip + +A VoIP message has been received from the server +===================== +*/ +static void CL_ParseVoip( msg_t *msg, bool ignoreData ) +{ + static short decoded[VOIP_MAX_PACKET_SAMPLES*4]; // !!! FIXME: don't hard code + + const int sender = MSG_ReadShort(msg); + const int generation = MSG_ReadByte(msg); + const int sequence = MSG_ReadLong(msg); + const int frames = MSG_ReadByte(msg); + const int packetsize = MSG_ReadShort(msg); + int flags = VOIP_DIRECT; + if (clc.netchan.alternateProtocol == 0) + flags = MSG_ReadBits(msg, VOIP_FLAGCNT); + char encoded[4000]; + int numSamples; + int seqdiff; + int written = 0; + int i; + + Com_DPrintf("VoIP: %d-byte packet from client %d\n", packetsize, sender); + + if (sender < 0) + return; // short/invalid packet, bail. + else if (generation < 0) + return; // short/invalid packet, bail. + else if (sequence < 0) + return; // short/invalid packet, bail. + else if (frames < 0) + return; // short/invalid packet, bail. + else if (packetsize < 0) + return; // short/invalid packet, bail. + + if (packetsize > sizeof (encoded)) { // overlarge packet? + int bytesleft = packetsize; + while (bytesleft) { + int br = bytesleft; + if (br > sizeof (encoded)) + br = sizeof (encoded); + MSG_ReadData(msg, encoded, br); + bytesleft -= br; + } + return; // overlarge packet, bail. + } + + MSG_ReadData(msg, encoded, packetsize); + + if (ignoreData) { + return; // just ignore legacy speex voip data + } else if (!clc.voipCodecInitialized) { + return; // can't handle VoIP without libopus! + } else if (sender >= MAX_CLIENTS) { + return; // bogus sender. + } else if (CL_ShouldIgnoreVoipSender(sender)) { + return; // Channel is muted, bail. + } + + // !!! FIXME: make sure data is narrowband? Does decoder handle this? + + Com_DPrintf("VoIP: packet accepted!\n"); + + seqdiff = sequence - clc.voipIncomingSequence[sender]; + + // This is a new "generation" ... a new recording started, reset the bits. + if (generation != clc.voipIncomingGeneration[sender]) { + Com_DPrintf("VoIP: new generation %d!\n", generation); + opus_decoder_ctl(clc.opusDecoder[sender], OPUS_RESET_STATE); + clc.voipIncomingGeneration[sender] = generation; + seqdiff = 0; + } else if (seqdiff < 0) { // we're ahead of the sequence?! + // This shouldn't happen unless the packet is corrupted or something. + Com_DPrintf("VoIP: misordered sequence! %d < %d!\n", + sequence, clc.voipIncomingSequence[sender]); + // reset the decoder just in case. + opus_decoder_ctl(clc.opusDecoder[sender], OPUS_RESET_STATE); + seqdiff = 0; + } else if (seqdiff * VOIP_MAX_PACKET_SAMPLES*2 >= sizeof (decoded)) { // dropped more than we can handle? + // just start over. + Com_DPrintf("VoIP: Dropped way too many (%d) frames from client #%d\n", + seqdiff, sender); + opus_decoder_ctl(clc.opusDecoder[sender], OPUS_RESET_STATE); + seqdiff = 0; + } + + if (seqdiff != 0) { + Com_DPrintf("VoIP: Dropped %d frames from client #%d\n", + seqdiff, sender); + // tell opus that we're missing frames... + for (i = 0; i < seqdiff; i++) { + assert((written + VOIP_MAX_PACKET_SAMPLES) * 2 < sizeof (decoded)); + numSamples = opus_decode(clc.opusDecoder[sender], NULL, 0, decoded + written, VOIP_MAX_PACKET_SAMPLES, 0); + if ( numSamples <= 0 ) { + Com_DPrintf("VoIP: Error decoding frame %d from client #%d\n", i, sender); + continue; + } + written += numSamples; + } + } + + numSamples = opus_decode(clc.opusDecoder[sender], + (const unsigned char*)encoded, + packetsize, + decoded + written, + ARRAY_LEN(decoded) - written, + 0); + + if ( numSamples <= 0 ) { + Com_DPrintf("VoIP: Error decoding voip data from client #%d\n", sender); + numSamples = 0; + } + + #if 0 + static FILE *encio = NULL; + if (encio == NULL) encio = fopen("voip-incoming-encoded.bin", "wb"); + if (encio != NULL) { fwrite(encoded, len, 1, encio); fflush(encio); } + static FILE *decio = NULL; + if (decio == NULL) decio = fopen("voip-incoming-decoded.bin", "wb"); + if (decio != NULL) { fwrite(decoded+written, clc.speexFrameSize*2, 1, decio); fflush(decio); } + #endif + + written += numSamples; + + Com_DPrintf("VoIP: playback %d bytes, %d samples, %d frames\n", + written * 2, written, frames); + + if(written > 0) + CL_PlayVoip(sender, written, (const byte *) decoded, flags); + + clc.voipIncomingSequence[sender] = sequence + frames; +} +#endif + + +/* +===================== +CL_ParseCommandString + +Command strings are just saved off until cgame asks for them +when it transitions a snapshot +===================== +*/ +void CL_ParseCommandString( msg_t *msg ) { + char *s; + int seq; + int index; + + seq = MSG_ReadLong( msg ); + s = MSG_ReadString( msg ); + + // see if we have already executed stored it off + if ( clc.serverCommandSequence >= seq ) { + return; + } + clc.serverCommandSequence = seq; + + index = seq & (MAX_RELIABLE_COMMANDS-1); + Q_strncpyz( clc.serverCommands[ index ], s, sizeof( clc.serverCommands[ index ] ) ); +} + + +/* +===================== +CL_ParseServerMessage +===================== +*/ +void CL_ParseServerMessage( msg_t *msg ) { + int cmd; + + if ( cl_shownet->integer == 1 ) { + Com_Printf ("%i ",msg->cursize); + } else if ( cl_shownet->integer >= 2 ) { + Com_Printf ("------------------\n"); + } + + MSG_Bitstream(msg); + + // get the reliable sequence acknowledge number + clc.reliableAcknowledge = MSG_ReadLong( msg ); + // + if ( clc.reliableAcknowledge < clc.reliableSequence - MAX_RELIABLE_COMMANDS ) { + clc.reliableAcknowledge = clc.reliableSequence; + } + + // + // parse the message + // + while ( 1 ) { + if ( msg->readcount > msg->cursize ) { + Com_Error (ERR_DROP,"CL_ParseServerMessage: read past end of server message"); + break; + } + + cmd = MSG_ReadByte( msg ); + + if ( clc.netchan.alternateProtocol != 0 ) + { + // See if this is an extension command after the EOF, which means we + // got data that a legacy client should ignore. + if ( cmd == svc_EOF && MSG_LookaheadByte( msg ) == svc_voipSpeex ) { + SHOWNET( msg, "EXTENSION" ); + MSG_ReadByte( msg ); // throw the svc_extension byte away. + cmd = MSG_ReadByte( msg ); // something legacy clients can't do! + // sometimes you get a svc_extension at end of stream...dangling + // bits in the huffman decoder giving a bogus value? + if ( cmd == -1 ) { + cmd = svc_EOF; + } + } + + if ( cmd == svc_voipSpeex ) { + cmd = svc_voipSpeex + 1; + } else if ( cmd == svc_voipSpeex + 1 ) { + cmd = svc_voipSpeex; + } + } + + if (cmd == svc_EOF) { + SHOWNET( msg, "END OF MESSAGE" ); + break; + } + + if ( cl_shownet->integer >= 2 ) { + if ( (cmd < 0) || (!svc_strings[cmd]) ) { + Com_Printf( "%3i:BAD CMD %i\n", msg->readcount-1, cmd ); + } else { + SHOWNET( msg, svc_strings[cmd] ); + } + } + + // other commands + switch ( cmd ) { + default: + Com_Error (ERR_DROP,"CL_ParseServerMessage: Illegible server message"); + break; + case svc_nop: + break; + case svc_serverCommand: + CL_ParseCommandString( msg ); + break; + case svc_gamestate: + CL_ParseGamestate( msg ); + break; + case svc_snapshot: + CL_ParseSnapshot( msg ); + break; + case svc_download: + CL_ParseDownload( msg ); + break; + case svc_voipSpeex: +#ifdef USE_VOIP + CL_ParseVoip( msg, true ); +#endif + break; + case svc_voipOpus: +#ifdef USE_VOIP + CL_ParseVoip( msg, !clc.voipEnabled ); +#endif + break; + } + } +} diff --git a/src/client/cl_rest.cpp b/src/client/cl_rest.cpp new file mode 100644 index 0000000..c1ff2fc --- /dev/null +++ b/src/client/cl_rest.cpp @@ -0,0 +1,117 @@ +// +// Restclient wrapper abusively modified to be a one-off donwloader. +// -wtfbbqhax +// +// TODO Verify sha256 of file if exists + +#include "cl_rest.h" + +#include <unistd.h> + +#include <cerrno> +#include <cstring> +#include <fstream> +#include <iostream> +#include <vector> + +#include "qcommon/files.h" +#include "restclient/restclient.h" + +bool is_good(std::string filename, int permissions = (R_OK|W_OK)) +{ + int ret = access(filename.c_str(), permissions); + if (ret) + std::cerr << filename << ": " << strerror(errno) << std::endl; + + return !ret; +} + +bool MakeDir(std::string destdir, std::string basegame) +{ + std::string destpath(destdir); + + if ( basegame != "" ) + { + destpath += '/'; + destpath += basegame; + } + + if ( *destpath.rbegin() != '/' ) + destpath += '/'; // XXX FS_CreatePath requires a trailing slash. + // Maybe the assumption is that a file listing might be included? + + FS_CreatePath(destpath.c_str()); + return true; +} + +#include "sys/dialog.h" +static bool PromptDownloadPk3s(std::string basegame, const std::vector<std::string>& missing) +{ + std::string msg; + + msg = "The following files must be downloaded to complete the installation.\n\n"; + for ( auto f : missing ) + msg += "\t" + basegame + "/" + f + "\n"; + + msg += "\n"; + msg += "Yes to continue, No to quit the game."; + + if( Sys_Dialog( DT_YES_NO, msg.c_str(), "You're almost ready!" ) == DR_YES ) + return true; + + return false; +} + +bool GetTremulousPk3s(const char* destdir, const char* basegame) +{ + std::string baseuri = "https://github.com/wtfbbqhax/tremulous-data/raw/master/"; + std::vector<std::string> files = { + "data-gpp1.pk3", + "data-1.1.0.pk3", + "map-arachnid2-1.1.0.pk3", + "map-atcs-1.1.0.pk3", + "map-karith-1.1.0.pk3", + "map-nexus6-1.1.0.pk3", + "map-niveus-1.1.0.pk3", + "map-transit-1.1.0.pk3", + "map-tremor-1.1.0.pk3", + "map-uncreation-1.1.0.pk3" + }; + + RestClient::init(); + + MakeDir(destdir, basegame); + + if (!PromptDownloadPk3s(basegame, files)) + return false; + + for (auto f : files ) + { + std::string destpath(destdir); + destpath += "/"; + destpath += basegame; + destpath += "/"; + destpath += f; + + if ( is_good(destpath) ) + { + return false; + } + + std::cout << "Downloading " << baseuri << f << std::endl; + std::ofstream dl(destpath); + //dl.open(destpath); + if ( dl.fail() ) + { + std::cerr << "Error " << strerror(errno) << "\n"; + continue; + } + + RestClient::Response resp = RestClient::get(baseuri + f); + + dl << resp.body; + dl.close(); + } + + return true; +} diff --git a/src/client/cl_rest.h b/src/client/cl_rest.h new file mode 100644 index 0000000..afde7d2 --- /dev/null +++ b/src/client/cl_rest.h @@ -0,0 +1,18 @@ +#ifndef _CL_REST_H_ +#define _CL_REST_H_ + +bool GetPermissions(const char*execpath); + +// Download a fresh copy of the "required" base game files to the top-most fs_basegame. +// +// Returns true(1) on success +// Return false(0) on error +// +bool GetTremulousPk3s(const char* destdir, const char* basegame); + +#include <string> + +//bool is_good(std::string filename, int permissions = (R_OK|W_OK)); +bool MakeDir(std::string destdir, std::string basegame); + +#endif diff --git a/src/client/cl_scrn.cpp b/src/client/cl_scrn.cpp new file mode 100644 index 0000000..7d0c7e2 --- /dev/null +++ b/src/client/cl_scrn.cpp @@ -0,0 +1,588 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ +// cl_scrn.c -- master for refresh, status bar, console, chat, notify, etc + +#include "client.h" + +bool scr_initialized; // ready to draw + +cvar_t *cl_timegraph; +cvar_t *cl_debuggraph; +cvar_t *cl_graphheight; +cvar_t *cl_graphscale; +cvar_t *cl_graphshift; + +/* +================ +SCR_DrawNamedPic + +Coordinates are 640*480 virtual values +================= +*/ +void SCR_DrawNamedPic(float x, float y, float width, float height, const char *picname) +{ + qhandle_t hShader; + + assert(width != 0); + + hShader = re.RegisterShader(picname); + SCR_AdjustFrom640(&x, &y, &width, &height); + re.DrawStretchPic(x, y, width, height, 0, 0, 1, 1, hShader); +} + +/* +================ +SCR_AdjustFrom640 + +Adjusted for resolution and screen aspect ratio +================ +*/ +void SCR_AdjustFrom640(float *x, float *y, float *w, float *h) +{ + float xscale; + float yscale; + +#if 0 + // adjust for wide screens + if ( cls.glconfig.vidWidth * 480 > cls.glconfig.vidHeight * 640 ) { + *x += 0.5 * ( cls.glconfig.vidWidth - ( cls.glconfig.vidHeight * 640 / 480 ) ); + } +#endif + + // scale for screen sizes + xscale = cls.glconfig.vidWidth / 640.0; + yscale = cls.glconfig.vidHeight / 480.0; + if (x) + { + *x *= xscale; + } + if (y) + { + *y *= yscale; + } + if (w) + { + *w *= xscale; + } + if (h) + { + *h *= yscale; + } +} + +/* +================ +SCR_FillRect + +Coordinates are 640*480 virtual values +================= +*/ +void SCR_FillRect(float x, float y, float width, float height, const float *color) +{ + re.SetColor(color); + + SCR_AdjustFrom640(&x, &y, &width, &height); + re.DrawStretchPic(x, y, width, height, 0, 0, 0, 0, cls.whiteShader); + + re.SetColor(NULL); +} + +/* +================ +SCR_DrawPic + +Coordinates are 640*480 virtual values +================= +*/ +void SCR_DrawPic(float x, float y, float width, float height, qhandle_t hShader) +{ + SCR_AdjustFrom640(&x, &y, &width, &height); + re.DrawStretchPic(x, y, width, height, 0, 0, 1, 1, hShader); +} + +/* +** SCR_DrawChar +** chars are drawn at 640*480 virtual screen size +*/ +static void SCR_DrawChar(int x, int y, float size, int ch) +{ + int row, col; + float frow, fcol; + float ax, ay, aw, ah; + + ch &= 255; + + if (ch == ' ') + { + return; + } + + if (y < -size) + { + return; + } + + ax = x; + ay = y; + aw = size; + ah = size; + SCR_AdjustFrom640(&ax, &ay, &aw, &ah); + + row = ch >> 4; + col = ch & 15; + + frow = row * 0.0625; + fcol = col * 0.0625; + size = 0.0625; + + re.DrawStretchPic(ax, ay, aw, ah, fcol, frow, fcol + size, frow + size, cls.charSetShader); +} + +/* +** SCR_DrawSmallChar +** small chars are drawn at native screen resolution +*/ +void SCR_DrawSmallChar(int x, int y, int ch) +{ + int row, col; + float frow, fcol; + float size; + + ch &= 255; + + if (ch == ' ') + { + return; + } + + if (y < -SMALLCHAR_HEIGHT) + { + return; + } + + row = ch >> 4; + col = ch & 15; + + frow = row * 0.0625; + fcol = col * 0.0625; + size = 0.0625; + + re.DrawStretchPic(x, y, SMALLCHAR_WIDTH, SMALLCHAR_HEIGHT, fcol, frow, fcol + size, frow + size, cls.charSetShader); +} + +/* +================== +SCR_DrawBigString[Color] + +Draws a multi-colored string with a drop shadow, optionally forcing +to a fixed color. + +Coordinates are at 640 by 480 virtual resolution +================== +*/ +static void SCR_DrawStringExt( + int x, int y, float size, const char *string, float *setColor, bool forceColor, bool noColorEscape) +{ + vec4_t color; + const char *s; + int xx; + + // draw the drop shadow + color[0] = color[1] = color[2] = 0; + color[3] = setColor[3]; + re.SetColor(color); + s = string; + xx = x; + while (*s) + { + if (!noColorEscape && Q_IsColorString(s)) + { + s += 2; + continue; + } + SCR_DrawChar(xx + 2, y + 2, size, *s); + xx += size; + s++; + } + + // draw the colored text + s = string; + xx = x; + re.SetColor(setColor); + while (*s) + { + if (Q_IsColorString(s)) + { + if (!forceColor) + { + ::memcpy(color, g_color_table[ColorIndex(*(s + 1))], sizeof(color)); + color[3] = setColor[3]; + re.SetColor(color); + } + if (!noColorEscape) + { + s += 2; + continue; + } + } + SCR_DrawChar(xx, y, size, *s); + xx += size; + s++; + } + re.SetColor(NULL); +} + +void SCR_DrawBigString(int x, int y, const char *s, float alpha, bool noColorEscape) +{ + float color[4]; + + color[0] = color[1] = color[2] = 1.0; + color[3] = alpha; + SCR_DrawStringExt(x, y, BIGCHAR_WIDTH, s, color, false, noColorEscape); +} + +void SCR_DrawBigStringColor(int x, int y, const char *s, vec4_t color, bool noColorEscape) +{ + SCR_DrawStringExt(x, y, BIGCHAR_WIDTH, s, color, true, noColorEscape); +} + +/* +================== +SCR_DrawSmallString[Color] + +Draws a multi-colored string with a drop shadow, optionally forcing +to a fixed color. +================== +*/ +void SCR_DrawSmallStringExt(int x, int y, const char *string, float *setColor, bool forceColor, bool noColorEscape) +{ + vec4_t color; + const char *s; + int xx; + + // draw the colored text + s = string; + xx = x; + re.SetColor(setColor); + while (*s) + { + if (Q_IsColorString(s)) + { + if (!forceColor) + { + ::memcpy(color, g_color_table[ColorIndex(*(s + 1))], sizeof(color)); + color[3] = setColor[3]; + re.SetColor(color); + } + if (!noColorEscape) + { + s += 2; + continue; + } + } + SCR_DrawSmallChar(xx, y, *s); + xx += SMALLCHAR_WIDTH; + s++; + } + re.SetColor(NULL); +} + +/* +** SCR_Strlen -- skips color escape codes +*/ +static int SCR_Strlen(const char *str) +{ + const char *s = str; + int count = 0; + + while (*s) + { + if (Q_IsColorString(s)) + { + s += 2; + } + else + { + count++; + s++; + } + } + + return count; +} + +/* +** SCR_GetBigStringWidth +*/ +int SCR_GetBigStringWidth(const char *str) { return SCR_Strlen(str) * BIGCHAR_WIDTH; } +//=============================================================================== + +#ifdef USE_VOIP +/* +================= +SCR_DrawVoipMeter + +FIXME: inherited from ioq3, move to cgame/ui +================= +*/ +void SCR_DrawVoipMeter(void) +{ + char buffer[16]; + char string[256]; + int limit, i; + + if (!cl_voipShowMeter->integer) + return; // player doesn't want to show meter at all. + else if (!cl_voipSend->integer) + return; // not recording at the moment. + else if (clc.state != CA_ACTIVE) + return; // not connected to a server. + else if (!clc.voipEnabled) + return; // server doesn't support VoIP. + else if (clc.demoplaying) + return; // playing back a demo. + else if (!cl_voip->integer) + return; // client has VoIP support disabled. + + limit = (int)(clc.voipPower * 10.0f); + if (limit > 10) limit = 10; + + for (i = 0; i < limit; i++) buffer[i] = '*'; + while (i < 10) buffer[i++] = ' '; + buffer[i] = '\0'; + + sprintf(string, "VoIP: [%s]", buffer); + SCR_DrawStringExt(320 - strlen(string) * 4, 10, 8, string, g_color_table[7], true, false); +} +#endif + +/* +=============================================================================== + +DEBUG GRAPH + +=============================================================================== +*/ + +static int current; +static float values[1024]; + +/* +============== +SCR_DebugGraph +============== +*/ +void SCR_DebugGraph(float value) +{ + values[current] = value; + current = (current + 1) % ARRAY_LEN(values); +} + +/* +============== +SCR_DrawDebugGraph +============== +*/ +void SCR_DrawDebugGraph(void) +{ + int a, x, y, w, i, h; + float v; + + // + // draw the graph + // + w = cls.glconfig.vidWidth; + x = 0; + y = cls.glconfig.vidHeight; + re.SetColor(g_color_table[0]); + re.DrawStretchPic(x, y - cl_graphheight->integer, w, cl_graphheight->integer, 0, 0, 0, 0, cls.whiteShader); + re.SetColor(NULL); + + for (a = 0; a < w; a++) + { + i = (ARRAY_LEN(values) + current - 1 - (a % ARRAY_LEN(values))) % ARRAY_LEN(values); + v = values[i]; + v = v * cl_graphscale->integer + cl_graphshift->integer; + + if (v < 0) v += cl_graphheight->integer * (1 + (int)(-v / cl_graphheight->integer)); + h = (int)v % cl_graphheight->integer; + re.DrawStretchPic(x + w - 1 - a, y - h, 1, h, 0, 0, 0, 0, cls.whiteShader); + } +} + +//============================================================================= + +/* +================== +SCR_Init +================== +*/ +void SCR_Init(void) +{ + cl_timegraph = Cvar_Get("timegraph", "0", CVAR_CHEAT); + cl_debuggraph = Cvar_Get("debuggraph", "0", CVAR_CHEAT); + cl_graphheight = Cvar_Get("graphheight", "32", CVAR_CHEAT); + cl_graphscale = Cvar_Get("graphscale", "1", CVAR_CHEAT); + cl_graphshift = Cvar_Get("graphshift", "0", CVAR_CHEAT); + + scr_initialized = true; +} + +//======================================================= + +/* +================== +SCR_DrawScreenField + +This will be called twice if rendering in stereo mode +================== +*/ +void SCR_DrawScreenField(stereoFrame_t stereoFrame) +{ + bool uiFullscreen; + + re.BeginFrame(stereoFrame); + + uiFullscreen = (cls.ui && VM_Call(cls.ui, UI_IS_FULLSCREEN - (cls.uiInterface == 2 ? 2 : 0))); + + // wide aspect ratio screens need to have the sides cleared + // unless they are displaying game renderings + if (uiFullscreen || clc.state < CA_LOADING) + { + if (cls.glconfig.vidWidth * 480 > cls.glconfig.vidHeight * 640) + { + re.SetColor(g_color_table[0]); + re.DrawStretchPic(0, 0, cls.glconfig.vidWidth, cls.glconfig.vidHeight, 0, 0, 0, 0, cls.whiteShader); + re.SetColor(NULL); + } + } + + // if the menu is going to cover the entire screen, we + // don't need to render anything under it + if (cls.ui && !uiFullscreen) + { + switch (clc.state) + { + default: + Com_Error(ERR_FATAL, "SCR_DrawScreenField: bad clc.state"); + break; + case CA_CINEMATIC: + SCR_DrawCinematic(); + break; + case CA_DISCONNECTED: + // force menu up + S_StopAllSounds(); + VM_Call(cls.ui, UI_SET_ACTIVE_MENU - (cls.uiInterface == 2 ? 2 : 0), UIMENU_MAIN); + break; + case CA_CONNECTING: + case CA_CHALLENGING: + case CA_CONNECTED: + // connecting clients will only show the connection dialog + // refresh to update the time + VM_Call(cls.ui, UI_REFRESH - (cls.uiInterface == 2 ? 2 : 0), cls.realtime); + VM_Call(cls.ui, UI_DRAW_CONNECT_SCREEN - (cls.uiInterface == 2 ? 2 : 0), false); + break; + case CA_LOADING: + case CA_PRIMED: + // draw the game information screen and loading progress + CL_CGameRendering(stereoFrame); + break; + case CA_ACTIVE: + // always supply STEREO_CENTER as vieworg offset is now done by the engine. + CL_CGameRendering(stereoFrame); +#ifdef USE_VOIP + SCR_DrawVoipMeter(); +#endif + break; + } + } + + // the menu draws next + if (Key_GetCatcher() & KEYCATCH_UI && cls.ui) + { + VM_Call(cls.ui, UI_REFRESH - (cls.uiInterface == 2 ? 2 : 0), cls.realtime); + } + + // console draws next + Con_DrawConsole(); + + // debug graph can be drawn on top of anything + if (cl_debuggraph->integer || cl_timegraph->integer || cl_debugMove->integer) + { + SCR_DrawDebugGraph(); + } +} + +/* +================== +SCR_UpdateScreen + +This is called every frame, and can also be called explicitly to flush +text to the screen. +================== +*/ +void SCR_UpdateScreen(void) +{ + static int recursive; + + if (!scr_initialized) + { + return; // not initialized yet + } + + if (++recursive > 2) + { + Com_Error(ERR_FATAL, "SCR_UpdateScreen: recursively called"); + } + recursive = 1; + + // If there is no VM, there are also no rendering commands issued. Stop the renderer in + // that case. + if (cls.ui || com_dedicated->integer) + { + // XXX + int in_anaglyphMode = Cvar_VariableIntegerValue("r_anaglyphMode"); + // if running in stereo, we need to draw the frame twice + if (cls.glconfig.stereoEnabled || in_anaglyphMode) + { + SCR_DrawScreenField(STEREO_LEFT); + SCR_DrawScreenField(STEREO_RIGHT); + } + else + { + SCR_DrawScreenField(STEREO_CENTER); + } + + if (com_speeds->integer) + { + re.EndFrame(&time_frontend, &time_backend); + } + else + { + re.EndFrame(NULL, NULL); + } + } + + recursive = 0; +} diff --git a/src/client/cl_ui.cpp b/src/client/cl_ui.cpp new file mode 100644 index 0000000..4052fe6 --- /dev/null +++ b/src/client/cl_ui.cpp @@ -0,0 +1,1269 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#include "client.h" + +#include "cl_updates.h" + +/* +==================== +GetClientState +==================== +*/ +static void GetClientState(uiClientState_t *state) +{ + state->connectPacketCount = clc.connectPacketCount; + state->connState = clc.state; + Q_strncpyz(state->servername, clc.servername, sizeof(state->servername)); + Q_strncpyz(state->updateInfoString, cls.updateInfoString, sizeof(state->updateInfoString)); + Q_strncpyz(state->messageString, clc.serverMessage, sizeof(state->messageString)); + state->clientNum = (clc.netchan.alternateProtocol == 2 ? cl.snap.alternatePs.clientNum : cl.snap.ps.clientNum); +} + +/* +==================== +LAN_LoadCachedServers +==================== +*/ +void LAN_LoadCachedServers(void) +{ + int size; + fileHandle_t fileIn; + cls.numglobalservers = cls.numfavoriteservers = 0; + cls.numGlobalServerAddresses = 0; + if (FS_SV_FOpenFileRead("servercache.dat", &fileIn)) + { + FS_Read(&cls.numglobalservers, sizeof(int), fileIn); + FS_Read(&cls.numfavoriteservers, sizeof(int), fileIn); + FS_Read(&size, sizeof(int), fileIn); + if (size == sizeof(cls.globalServers) + sizeof(cls.favoriteServers)) + { + FS_Read(&cls.globalServers, sizeof(cls.globalServers), fileIn); + FS_Read(&cls.favoriteServers, sizeof(cls.favoriteServers), fileIn); + } + else + { + cls.numglobalservers = cls.numfavoriteservers = 0; + cls.numGlobalServerAddresses = 0; + } + FS_FCloseFile(fileIn); + } +} + +/* +==================== +LAN_SaveServersToCache +==================== +*/ +void LAN_SaveServersToCache(void) +{ + fileHandle_t fileOut = FS_SV_FOpenFileWrite("servercache.dat"); + FS_Write(&cls.numglobalservers, sizeof(int), fileOut); + FS_Write(&cls.numfavoriteservers, sizeof(int), fileOut); + + int size = sizeof(cls.globalServers) + sizeof(cls.favoriteServers); + FS_Write(&size, sizeof(int), fileOut); + FS_Write(&cls.globalServers, sizeof(cls.globalServers), fileOut); + FS_Write(&cls.favoriteServers, sizeof(cls.favoriteServers), fileOut); + FS_FCloseFile(fileOut); +} + +/* +==================== +GetNews +==================== +*/ +static bool GetNews(bool begin) +{ + bool finished = false; + fileHandle_t fileIn; + int readSize; + + if (begin) + { // if not already using curl, start the download + if (!clc.downloadCURLM) + { + if (!CL_cURL_Init()) + { + Cvar_Set("cl_newsString", "^1Error: Could not load cURL library"); + return true; + } + clc.activeCURLNotGameRelated = true; + CL_cURL_BeginDownload("news.dat", "http://grangerhub.com/wp-content/uploads/clientnews.txt"); + return false; + } + } + + if (!clc.downloadCURLM && FS_SV_FOpenFileRead("news.dat", &fileIn)) + { + readSize = FS_Read(clc.newsString, sizeof(clc.newsString), fileIn); + FS_FCloseFile(fileIn); + clc.newsString[readSize] = '\0'; + if (readSize > 0) + { + finished = true; + clc.cURLUsed = false; + CL_cURL_Shutdown(); + clc.activeCURLNotGameRelated = false; + } + } + if (!finished) strcpy(clc.newsString, "Retrieving..."); + Cvar_Set("cl_newsString", clc.newsString); + return finished; + Cvar_Set("cl_newsString", "^1You must compile your client with CURL support to use this feature"); + return true; +} + +/* +==================== +LAN_ResetPings +==================== +*/ +static void LAN_ResetPings(int source) +{ + int count, i; + serverInfo_t *servers = NULL; + count = 0; + + switch (source) + { + case AS_LOCAL: + servers = &cls.localServers[0]; + count = MAX_OTHER_SERVERS; + break; + case AS_MPLAYER: + case AS_GLOBAL: + servers = &cls.globalServers[0]; + count = MAX_GLOBAL_SERVERS; + break; + case AS_FAVORITES: + servers = &cls.favoriteServers[0]; + count = MAX_OTHER_SERVERS; + break; + } + if (servers) + { + for (i = 0; i < count; i++) + { + servers[i].ping = -1; + } + } +} + +/* +==================== +LAN_AddServer +==================== +*/ +static int LAN_AddServer(int source, const char *name, const char *address) +{ + int max, *count, i; + netadr_t adr; + serverInfo_t *servers = NULL; + max = MAX_OTHER_SERVERS; + count = NULL; + + switch (source) + { + case AS_LOCAL: + count = &cls.numlocalservers; + servers = &cls.localServers[0]; + break; + case AS_MPLAYER: + case AS_GLOBAL: + max = MAX_GLOBAL_SERVERS; + count = &cls.numglobalservers; + servers = &cls.globalServers[0]; + break; + case AS_FAVORITES: + count = &cls.numfavoriteservers; + servers = &cls.favoriteServers[0]; + break; + } + if (servers && *count < max) + { + NET_StringToAdr(address, &adr, NA_UNSPEC); + for (i = 0; i < *count; i++) + { + if (NET_CompareAdr(servers[i].adr, adr)) + { + break; + } + } + if (i >= *count) + { + servers[*count].adr = adr; + Q_strncpyz(servers[*count].hostName, name, sizeof(servers[*count].hostName)); + servers[*count].visible = true; + (*count)++; + return 1; + } + return 0; + } + return -1; +} + +/* +==================== +LAN_RemoveServer +==================== +*/ +static void LAN_RemoveServer(int source, const char *addr) +{ + int *count, i; + serverInfo_t *servers = NULL; + count = NULL; + switch (source) + { + case AS_LOCAL: + count = &cls.numlocalservers; + servers = &cls.localServers[0]; + break; + case AS_MPLAYER: + case AS_GLOBAL: + count = &cls.numglobalservers; + servers = &cls.globalServers[0]; + break; + case AS_FAVORITES: + count = &cls.numfavoriteservers; + servers = &cls.favoriteServers[0]; + break; + } + if (servers) + { + netadr_t comp; + NET_StringToAdr(addr, &comp, NA_UNSPEC); + for (i = 0; i < *count; i++) + { + if (NET_CompareAdr(comp, servers[i].adr)) + { + int j = i; + while (j < *count - 1) + { + ::memcpy(&servers[j], &servers[j + 1], sizeof(servers[j])); + j++; + } + (*count)--; + break; + } + } + } +} + +/* +==================== +LAN_GetServerCount +==================== +*/ +static int LAN_GetServerCount(int source) +{ + switch (source) + { + case AS_LOCAL: + return cls.numlocalservers; + break; + case AS_MPLAYER: + case AS_GLOBAL: + return cls.numglobalservers; + break; + case AS_FAVORITES: + return cls.numfavoriteservers; + break; + } + return 0; +} + +/* +==================== +LAN_GetLocalServerAddressString +==================== +*/ +static void LAN_GetServerAddressString(int source, int n, char *buf, int buflen) +{ + switch (source) + { + case AS_LOCAL: + if (n >= 0 && n < MAX_OTHER_SERVERS) + { + Q_strncpyz(buf, NET_AdrToStringwPort(cls.localServers[n].adr), buflen); + if (cls.localServers[n].adr.alternateProtocol != 0) + Q_strncpyz(buf + (int)strlen(buf), (cls.localServers[n].adr.alternateProtocol == 1 ? " -g" : " -1"), + buflen - (int)strlen(buf)); + return; + } + break; + case AS_MPLAYER: + case AS_GLOBAL: + if (n >= 0 && n < MAX_GLOBAL_SERVERS) + { + Q_strncpyz(buf, NET_AdrToStringwPort(cls.globalServers[n].adr), buflen); + if (cls.globalServers[n].adr.alternateProtocol != 0) + Q_strncpyz(buf + (int)strlen(buf), + (cls.globalServers[n].adr.alternateProtocol == 1 ? " -g" : " -1"), buflen - (int)strlen(buf)); + return; + } + break; + case AS_FAVORITES: + if (n >= 0 && n < MAX_OTHER_SERVERS) + { + Q_strncpyz(buf, NET_AdrToStringwPort(cls.favoriteServers[n].adr), buflen); + if (cls.favoriteServers[n].adr.alternateProtocol != 0) + Q_strncpyz(buf + (int)strlen(buf), + (cls.favoriteServers[n].adr.alternateProtocol == 1 ? " -g" : " -1"), buflen - (int)strlen(buf)); + return; + } + break; + } + buf[0] = '\0'; +} + +/* +==================== +LAN_GetServerInfo +==================== +*/ +static void LAN_GetServerInfo(int source, int n, char *buf, int buflen) +{ + char info[MAX_STRING_CHARS]; + serverInfo_t *server = NULL; + info[0] = '\0'; + switch (source) + { + case AS_LOCAL: + if (n >= 0 && n < MAX_OTHER_SERVERS) + { + server = &cls.localServers[n]; + } + break; + case AS_MPLAYER: + case AS_GLOBAL: + if (n >= 0 && n < MAX_GLOBAL_SERVERS) + { + server = &cls.globalServers[n]; + } + break; + case AS_FAVORITES: + if (n >= 0 && n < MAX_OTHER_SERVERS) + { + server = &cls.favoriteServers[n]; + } + break; + } + if (server && buf) + { + buf[0] = '\0'; + if (server->adr.alternateProtocol != 0) + { + char hn[MAX_HOSTNAME_LENGTH]; + Q_strncpyz(hn, server->hostName, sizeof(hn)); + Q_strcat( + hn, sizeof(hn), (server->adr.alternateProtocol == 1 ? S_COLOR_WHITE " [GPP]" : S_COLOR_WHITE " [1.1]")); + Info_SetValueForKey(info, "hostname", hn); + } + else + { + Info_SetValueForKey(info, "hostname", server->hostName); + } + Info_SetValueForKey(info, "mapname", server->mapName); + Info_SetValueForKey(info, "label", server->label); + Info_SetValueForKey(info, "clients", va("%i", server->clients)); + Info_SetValueForKey(info, "sv_maxclients", va("%i", server->maxClients)); + Info_SetValueForKey(info, "ping", va("%i", server->ping)); + Info_SetValueForKey(info, "minping", va("%i", server->minPing)); + Info_SetValueForKey(info, "maxping", va("%i", server->maxPing)); + Info_SetValueForKey(info, "game", server->game); + Info_SetValueForKey(info, "gametype", va("%i", server->gameType)); + Info_SetValueForKey(info, "nettype", va("%i", server->netType)); + Info_SetValueForKey(info, "addr", NET_AdrToStringwPort(server->adr)); + Q_strncpyz(buf, info, buflen); + } + else + { + if (buf) + { + buf[0] = '\0'; + } + } +} + +/* +==================== +LAN_GetServerPing +==================== +*/ +static int LAN_GetServerPing(int source, int n) +{ + serverInfo_t *server = NULL; + switch (source) + { + case AS_LOCAL: + if (n >= 0 && n < MAX_OTHER_SERVERS) + { + server = &cls.localServers[n]; + } + break; + case AS_MPLAYER: + case AS_GLOBAL: + if (n >= 0 && n < MAX_GLOBAL_SERVERS) + { + server = &cls.globalServers[n]; + } + break; + case AS_FAVORITES: + if (n >= 0 && n < MAX_OTHER_SERVERS) + { + server = &cls.favoriteServers[n]; + } + break; + } + if (server) + { + return server->ping; + } + return -1; +} + +/* +==================== +LAN_GetServerPtr +==================== +*/ +static serverInfo_t *LAN_GetServerPtr(int source, int n) +{ + switch (source) + { + case AS_LOCAL: + if (n >= 0 && n < MAX_OTHER_SERVERS) + { + return &cls.localServers[n]; + } + break; + case AS_MPLAYER: + case AS_GLOBAL: + if (n >= 0 && n < MAX_GLOBAL_SERVERS) + { + return &cls.globalServers[n]; + } + break; + case AS_FAVORITES: + if (n >= 0 && n < MAX_OTHER_SERVERS) + { + return &cls.favoriteServers[n]; + } + break; + } + return NULL; +} + +#define FEATURED_MAXPING 200 +/* +==================== +LAN_CompareServers +==================== +*/ +static int LAN_CompareServers(int source, int sortKey, int sortDir, int s1, int s2) +{ + int res; + serverInfo_t *server1, *server2; + + server1 = LAN_GetServerPtr(source, s1); + server2 = LAN_GetServerPtr(source, s2); + if (!server1 || !server2) + { + return 0; + } + + // featured servers on top + if ((server1->label[0] && server1->ping <= FEATURED_MAXPING) || + (server2->label[0] && server2->ping <= FEATURED_MAXPING)) + { + res = Q_stricmpn(server1->label, server2->label, MAX_FEATLABEL_CHARS); + if (res) return -res; + } + + res = 0; + switch (sortKey) + { + case SORT_HOST: + { + char hostName1[MAX_HOSTNAME_LENGTH]; + char hostName2[MAX_HOSTNAME_LENGTH]; + char *p; + int i; + + for (p = server1->hostName, i = 0; *p != '\0'; p++) + { + if (Q_isalpha(*p)) hostName1[i++] = *p; + } + hostName1[i] = '\0'; + + for (p = server2->hostName, i = 0; *p != '\0'; p++) + { + if (Q_isalpha(*p)) hostName2[i++] = *p; + } + hostName2[i] = '\0'; + + res = Q_stricmp(hostName1, hostName2); + } + break; + + case SORT_GAME: + res = Q_stricmp(server1->game, server2->game); + break; + case SORT_MAP: + res = Q_stricmp(server1->mapName, server2->mapName); + break; + case SORT_CLIENTS: + if (server1->clients < server2->clients) + { + res = -1; + } + else if (server1->clients > server2->clients) + { + res = 1; + } + else + { + res = 0; + } + break; + case SORT_PING: + if (server1->ping < server2->ping) + { + res = -1; + } + else if (server1->ping > server2->ping) + { + res = 1; + } + else + { + res = 0; + } + break; + } + + if (sortDir) + { + if (res < 0) return 1; + if (res > 0) return -1; + return 0; + } + return res; +} + +/* +==================== +LAN_GetPingQueueCount +==================== +*/ +static int LAN_GetPingQueueCount(void) { return (CL_GetPingQueueCount()); } +/* +==================== +LAN_ClearPing +==================== +*/ +static void LAN_ClearPing(int n) { CL_ClearPing(n); } +/* +==================== +LAN_GetPing +==================== +*/ +static void LAN_GetPing(int n, char *buf, int buflen, int *pingtime) { CL_GetPing(n, buf, buflen, pingtime); } +/* +==================== +LAN_GetPingInfo +==================== +*/ +static void LAN_GetPingInfo(int n, char *buf, int buflen) { CL_GetPingInfo(n, buf, buflen); } +/* +==================== +LAN_MarkServerVisible +==================== +*/ +static void LAN_MarkServerVisible(int source, int n, bool visible) +{ + if (n == -1) + { + int count = MAX_OTHER_SERVERS; + serverInfo_t *server = NULL; + switch (source) + { + case AS_LOCAL: + server = &cls.localServers[0]; + break; + case AS_MPLAYER: + case AS_GLOBAL: + server = &cls.globalServers[0]; + count = MAX_GLOBAL_SERVERS; + break; + case AS_FAVORITES: + server = &cls.favoriteServers[0]; + break; + } + if (server) + { + for (n = 0; n < count; n++) + { + server[n].visible = visible; + } + } + } + else + { + switch (source) + { + case AS_LOCAL: + if (n >= 0 && n < MAX_OTHER_SERVERS) + { + cls.localServers[n].visible = visible; + } + break; + case AS_MPLAYER: + case AS_GLOBAL: + if (n >= 0 && n < MAX_GLOBAL_SERVERS) + { + cls.globalServers[n].visible = visible; + } + break; + case AS_FAVORITES: + if (n >= 0 && n < MAX_OTHER_SERVERS) + { + cls.favoriteServers[n].visible = visible; + } + break; + } + } +} + +/* +======================= +LAN_ServerIsVisible +======================= +*/ +static bool LAN_ServerIsVisible(int source, int n) +{ + switch (source) + { + case AS_LOCAL: + if (n >= 0 && n < MAX_OTHER_SERVERS) + { + return cls.localServers[n].visible; + } + break; + case AS_MPLAYER: + case AS_GLOBAL: + if (n >= 0 && n < MAX_GLOBAL_SERVERS) + { + return cls.globalServers[n].visible; + } + break; + case AS_FAVORITES: + if (n >= 0 && n < MAX_OTHER_SERVERS) + { + return cls.favoriteServers[n].visible; + } + break; + } + return false; +} + +/* +======================= +LAN_UpdateVisiblePings +======================= +*/ +static bool LAN_UpdateVisiblePings(int source) { return CL_UpdateVisiblePings_f(source); } +/* +==================== +LAN_GetServerStatus +==================== +*/ +static bool LAN_GetServerStatus(char *serverAddress, char *serverStatus, int maxLen) +{ + return CL_ServerStatus(serverAddress, serverStatus, maxLen); +} + +/* +==================== +CL_GetGlConfig +==================== +*/ +static void CL_GetGlconfig(glconfig_t *config) { *config = cls.glconfig; } +/* +==================== +GetConfigString +==================== +*/ +static bool GetConfigString(int i, char *buf, int size) +{ + int offset; + + if (i < 0 || i >= MAX_CONFIGSTRINGS) return false; + + offset = cl.gameState.stringOffsets[i]; + if (!offset) + { + if (size) buf[0] = '\0'; + return false; + } + + Q_strncpyz(buf, cl.gameState.stringData + offset, size); + + return true; +} + +/* +==================== +FloatAsInt +==================== +*/ +static int FloatAsInt(float f) +{ + floatint_t fi; + fi.f = f; + return fi.i; +} + +static bool probingUI = false; + +/* +==================== +CL_UISystemCalls + +The ui module is making a system call +==================== +*/ +intptr_t CL_UISystemCalls(intptr_t *args) +{ + if (cls.uiInterface == 2) + { + if (args[0] >= UI_R_SETCLIPREGION && args[0] < UI_MEMSET) + { + if (args[0] < UI_S_STOPBACKGROUNDTRACK - 1) + { + args[0] += 1; + } + else if (args[0] < UI_S_STOPBACKGROUNDTRACK + 4) + { + args[0] += UI_PARSE_ADD_GLOBAL_DEFINE - UI_S_STOPBACKGROUNDTRACK + 1; + } + else if (args[0] >= UI_PARSE_ADD_GLOBAL_DEFINE + 4) + { + args[0] += UI_GETNEWS - UI_PARSE_ADD_GLOBAL_DEFINE - 5; + if (args[0] == UI_PARSE_SOURCE_FILE_AND_LINE || args[0] == UI_GETNEWS) + args[0] = UI_PARSE_SOURCE_FILE_AND_LINE - 1337 - args[0]; + } + else + { + args[0] -= 4; + } + } + + switch (args[0]) + { + case UI_LAN_GETSERVERCOUNT: + case UI_LAN_GETSERVERADDRESSSTRING: + case UI_LAN_GETSERVERINFO: + case UI_LAN_MARKSERVERVISIBLE: + case UI_LAN_UPDATEVISIBLEPINGS: + case UI_LAN_RESETPINGS: + case UI_LAN_ADDSERVER: + case UI_LAN_REMOVESERVER: + case UI_LAN_GETSERVERPING: + case UI_LAN_SERVERISVISIBLE: + case UI_LAN_COMPARESERVERS: + if (args[1] == AS_GLOBAL) + { + args[1] = AS_LOCAL; + } + else if (args[1] == AS_LOCAL) + { + args[1] = AS_GLOBAL; + } + } + } + + switch (args[0]) + { + case UI_ERROR: + if (probingUI) + { + cls.uiInterface = 2; + return 0; + } + Com_Error(ERR_DROP, "%s", (const char *)VMA(1)); + return 0; + + case UI_PRINT: + Com_Printf("%s", (const char *)VMA(1)); + return 0; + + case UI_MILLISECONDS: + return Sys_Milliseconds(); + + case UI_CVAR_REGISTER: + Cvar_Register((vmCvar_t *)VMA(1), (const char *)VMA(2), (const char *)VMA(3), args[4]); + return 0; + + case UI_CVAR_UPDATE: + Cvar_Update((vmCvar_t *)VMA(1)); + return 0; + + case UI_CVAR_SET: + Cvar_SetSafe((const char *)VMA(1), (const char *)VMA(2)); + return 0; + + case UI_CVAR_VARIABLEVALUE: + return FloatAsInt(Cvar_VariableValue((const char *)VMA(1))); + + case UI_CVAR_VARIABLESTRINGBUFFER: + Cvar_VariableStringBuffer((const char *)VMA(1), (char *)VMA(2), args[3]); + return 0; + + case UI_CVAR_SETVALUE: + Cvar_SetValueSafe((const char *)VMA(1), VMF(2)); + return 0; + + case UI_CVAR_RESET: + Cvar_Reset((const char *)VMA(1)); + return 0; + + case UI_CVAR_CREATE: + Cvar_Register(NULL, (const char *)VMA(1), (const char *)VMA(2), args[3]); + return 0; + + case UI_CVAR_INFOSTRINGBUFFER: + Cvar_InfoStringBuffer(args[1], (char *)VMA(2), args[3]); + return 0; + + case UI_ARGC: + return Cmd_Argc(); + + case UI_ARGV: + Cmd_ArgvBuffer(args[1], (char *)VMA(2), args[3]); + return 0; + + case UI_CMD_EXECUTETEXT: + if (args[1] == EXEC_NOW) + { + if (!strncmp((const char *)VMA(2), "snd_restart", 11) + || !strncmp((const char *)VMA(2), "vid_restart", 11) + || !strncmp((const char *)VMA(2), "quit", 5)) + { + Com_Printf(S_COLOR_YELLOW "turning EXEC_NOW '%.11s' into EXEC_INSERT\n", (const char *)VMA(2)); + args[1] = EXEC_INSERT; + } + } + + // TODO: Do this better + if (!strncmp((const char *)VMA(2), "checkForUpdate", 14)) + { + CL_GetLatestRelease(); + return 0; + } + + Cbuf_ExecuteText(args[1], (const char *)VMA(2)); + return 0; + + case UI_FS_FOPENFILE: + return FS_FOpenFileByMode((const char *)VMA(1), (fileHandle_t *)VMA(2), (FS_Mode)args[3]); + + case UI_FS_READ: + FS_Read(VMA(1), args[2], args[3]); + return 0; + + case UI_FS_WRITE: + FS_Write(VMA(1), args[2], args[3]); + return 0; + + case UI_FS_FCLOSEFILE: + FS_FCloseFile(args[1]); + return 0; + + case UI_FS_GETFILELIST: + return FS_GetFileList((const char *)VMA(1), (const char *)VMA(2), (char *)VMA(3), args[4]); + + case UI_FS_SEEK: + return FS_Seek((fileHandle_t)args[1], args[2], (FS_Origin)args[3]); + + case UI_R_REGISTERMODEL: + return re.RegisterModel((const char *)VMA(1)); + + case UI_R_REGISTERSKIN: + return re.RegisterSkin((const char *)VMA(1)); + + case UI_R_REGISTERSHADERNOMIP: + return re.RegisterShaderNoMip((const char *)VMA(1)); + + case UI_R_CLEARSCENE: + re.ClearScene(); + return 0; + + case UI_R_ADDREFENTITYTOSCENE: + re.AddRefEntityToScene((const refEntity_t *)VMA(1)); + return 0; + + case UI_R_ADDPOLYTOSCENE: + re.AddPolyToScene(args[1], args[2], (const polyVert_t *)VMA(3), 1); + return 0; + + case UI_R_ADDLIGHTTOSCENE: + re.AddLightToScene((const float *)VMA(1), VMF(2), VMF(3), VMF(4), VMF(5)); + return 0; + + case UI_R_RENDERSCENE: + re.RenderScene((const refdef_t *)VMA(1)); + return 0; + + case UI_R_SETCOLOR: + re.SetColor((const float *)VMA(1)); + return 0; + + case UI_R_SETCLIPREGION: + re.SetClipRegion((const float *)VMA(1)); + return 0; + + case UI_R_DRAWSTRETCHPIC: + re.DrawStretchPic(VMF(1), VMF(2), VMF(3), VMF(4), VMF(5), VMF(6), VMF(7), VMF(8), args[9]); + return 0; + + case UI_R_MODELBOUNDS: + re.ModelBounds(args[1], (float *)VMA(2), (float *)VMA(3)); + return 0; + + case UI_UPDATESCREEN: + SCR_UpdateScreen(); + return 0; + + case UI_CM_LERPTAG: + re.LerpTag((orientation_t *)VMA(1), args[2], args[3], args[4], VMF(5), (const char *)VMA(6)); + return 0; + + case UI_S_REGISTERSOUND: + return S_RegisterSound((const char *)VMA(1), (bool)args[2]); + + case UI_S_STARTLOCALSOUND: + S_StartLocalSound(args[1], args[2]); + return 0; + + case UI_KEY_KEYNUMTOSTRINGBUF: + Key_KeynumToStringBuf(args[1], (char *)VMA(2), args[3]); + return 0; + + case UI_KEY_GETBINDINGBUF: + Key_GetBindingBuf(args[1], (char *)VMA(2), args[3]); + return 0; + + case UI_KEY_SETBINDING: + Key_SetBinding(args[1], (const char *)VMA(2)); + return 0; + + case UI_KEY_ISDOWN: + return Key_IsDown(args[1]); + + case UI_KEY_GETOVERSTRIKEMODE: + return Key_GetOverstrikeMode(); + + case UI_KEY_SETOVERSTRIKEMODE: + Key_SetOverstrikeMode((bool)args[1]); + return 0; + + case UI_KEY_CLEARSTATES: + Key_ClearStates(); + return 0; + + case UI_KEY_GETCATCHER: + return Key_GetCatcher(); + + case UI_KEY_SETCATCHER: + // don't allow the ui module to toggle the console + Key_SetCatcher((args[1] & ~KEYCATCH_CONSOLE) | (Key_GetCatcher() & KEYCATCH_CONSOLE)); + return 0; + + case UI_GETCLIPBOARDDATA: + ((char *)VMA(1))[0] = '\0'; + return 0; + + case UI_GETCLIENTSTATE: + GetClientState((uiClientState_t *)VMA(1)); + return 0; + + case UI_GETGLCONFIG: + CL_GetGlconfig((glconfig_t *)VMA(1)); + return 0; + + case UI_GETCONFIGSTRING: + return GetConfigString(args[1], (char *)VMA(2), args[3]); + + case UI_LAN_LOADCACHEDSERVERS: + LAN_LoadCachedServers(); + return 0; + + case UI_LAN_SAVECACHEDSERVERS: + LAN_SaveServersToCache(); + return 0; + + case UI_LAN_ADDSERVER: + return LAN_AddServer(args[1], (const char *)VMA(2), (const char *)VMA(3)); + + case UI_LAN_REMOVESERVER: + LAN_RemoveServer(args[1], (const char *)VMA(2)); + return 0; + + case UI_LAN_GETPINGQUEUECOUNT: + return LAN_GetPingQueueCount(); + + case UI_LAN_CLEARPING: + LAN_ClearPing(args[1]); + return 0; + + case UI_LAN_GETPING: + LAN_GetPing(args[1], (char *)VMA(2), args[3], (int *)VMA(4)); + return 0; + + case UI_LAN_GETPINGINFO: + LAN_GetPingInfo(args[1], (char *)VMA(2), args[3]); + return 0; + + case UI_LAN_GETSERVERCOUNT: + return LAN_GetServerCount(args[1]); + + case UI_LAN_GETSERVERADDRESSSTRING: + LAN_GetServerAddressString(args[1], args[2], (char *)VMA(3), args[4]); + return 0; + + case UI_LAN_GETSERVERINFO: + LAN_GetServerInfo(args[1], args[2], (char *)VMA(3), args[4]); + return 0; + + case UI_LAN_GETSERVERPING: + return LAN_GetServerPing(args[1], args[2]); + + case UI_LAN_MARKSERVERVISIBLE: + LAN_MarkServerVisible(args[1], args[2], (bool)args[3]); + return 0; + + case UI_LAN_SERVERISVISIBLE: + return LAN_ServerIsVisible(args[1], args[2]); + + case UI_LAN_UPDATEVISIBLEPINGS: + return LAN_UpdateVisiblePings(args[1]); + + case UI_LAN_RESETPINGS: + LAN_ResetPings(args[1]); + return 0; + + case UI_LAN_SERVERSTATUS: + return LAN_GetServerStatus((char *)VMA(1), (char *)VMA(2), args[3]); + + case UI_GETNEWS: + return GetNews((bool)args[1]); + + case UI_LAN_COMPARESERVERS: + return LAN_CompareServers(args[1], args[2], args[3], args[4], args[5]); + + case UI_MEMORY_REMAINING: + return Hunk_MemoryRemaining(); + + case UI_SET_PBCLSTATUS: + return 0; + + case UI_R_REGISTERFONT: + re.RegisterFont((const char *)VMA(1), args[2], (fontInfo_t *)VMA(3)); + return 0; + + case UI_MEMSET: + ::memset(VMA(1), args[2], args[3]); + return 0; + + case UI_MEMCPY: + ::memcpy(VMA(1), VMA(2), args[3]); + return 0; + + case UI_STRNCPY: + strncpy((char *)VMA(1), (const char *)VMA(2), args[3]); + return args[1]; + + case UI_SIN: + return FloatAsInt(sin(VMF(1))); + + case UI_COS: + return FloatAsInt(cos(VMF(1))); + + case UI_ATAN2: + return FloatAsInt(atan2(VMF(1), VMF(2))); + + case UI_SQRT: + return FloatAsInt(sqrt(VMF(1))); + + case UI_FLOOR: + return FloatAsInt(floor(VMF(1))); + + case UI_CEIL: + return FloatAsInt(ceil(VMF(1))); + + case UI_PARSE_ADD_GLOBAL_DEFINE: + return Parse_AddGlobalDefine((char *)VMA(1)); + case UI_PARSE_LOAD_SOURCE: + return Parse_LoadSourceHandle((char *)VMA(1)); + case UI_PARSE_FREE_SOURCE: + return Parse_FreeSourceHandle(args[1]); + case UI_PARSE_READ_TOKEN: + return Parse_ReadTokenHandle(args[1], (pc_token_t *)VMA(2)); + case UI_PARSE_SOURCE_FILE_AND_LINE: + return Parse_SourceFileAndLine(args[1], (char *)VMA(2), (int *)VMA(3)); + + case UI_S_STOPBACKGROUNDTRACK: + S_StopBackgroundTrack(); + return 0; + case UI_S_STARTBACKGROUNDTRACK: + S_StartBackgroundTrack((const char *)VMA(1), (const char *)VMA(2)); + return 0; + + case UI_REAL_TIME: + return Com_RealTime((qtime_t *)VMA(1)); + + case UI_CIN_PLAYCINEMATIC: + return CIN_PlayCinematic((const char *)VMA(1), args[2], args[3], args[4], args[5], args[6]); + + case UI_CIN_STOPCINEMATIC: + return CIN_StopCinematic(args[1]); + + case UI_CIN_RUNCINEMATIC: + return CIN_RunCinematic(args[1]); + + case UI_CIN_DRAWCINEMATIC: + CIN_DrawCinematic(args[1]); + return 0; + + case UI_CIN_SETEXTENTS: + CIN_SetExtents(args[1], args[2], args[3], args[4], args[5]); + return 0; + + case UI_R_REMAP_SHADER: + re.RemapShader((const char *)VMA(1), (const char *)VMA(2), (const char *)VMA(3)); + return 0; + + default: + Com_Error(ERR_DROP, "Bad UI system trap: %ld", (long int)args[0]); + } + + return 0; +} + +/* +==================== +CL_ShutdownUI +==================== +*/ +void CL_ShutdownUI(void) +{ + Key_SetCatcher(Key_GetCatcher() & ~KEYCATCH_UI); + cls.uiStarted = false; + if (!cls.ui) + { + return; + } + VM_Call(cls.ui, UI_SHUTDOWN); + VM_Free(cls.ui); + cls.ui = NULL; +} + +/* +==================== +CL_InitUI +==================== +*/ +void CL_InitUI(void) +{ + // load the dll or bytecode + vmInterpret_t interpret = (vmInterpret_t)Cvar_VariableValue("vm_ui"); + if (cl_connectedToPureServer) + { + // if sv_pure is set we only allow qvms to be loaded + if (interpret != VMI_COMPILED && interpret != VMI_BYTECODE) interpret = VMI_COMPILED; + } + + cls.ui = VM_Create("ui", CL_UISystemCalls, interpret); + if (!cls.ui) + { + Com_Printf("Failed to find a valid UI vm. The following paths were searched:\n"); + Cmd_ExecuteString("path /\n"); + Com_Error(ERR_RECONNECT, "VM_Create on UI failed"); + } + + // sanity check + int v = VM_Call(cls.ui, UI_GETAPIVERSION); + if (v != UI_API_VERSION) + { + // Free cls.ui now, so UI_SHUTDOWN doesn't get called later. + VM_Free(cls.ui); + cls.ui = NULL; + + cls.uiStarted = false; + Com_Error(ERR_DROP, "User Interface is version %d, expected %d", v, UI_API_VERSION); + } + + // Probe UI interface + // Calls the GPP UI_CONSOLE_COMMAND (10), if GPP will return false 0. If a 1.1.0 qvm, will hit the error handler. + Cmd_TokenizeString(""); + cls.uiInterface = 0; + probingUI = true; + if ( VM_Call(cls.ui, UI_CONSOLE_COMMAND, 0) < 0 ) + cls.uiInterface = 2; + + probingUI = false; + + if (clc.state >= CA_CONNECTED && clc.state <= CA_ACTIVE && + (clc.netchan.alternateProtocol == 2) != (cls.uiInterface == 2)) + { + Com_Printf(S_COLOR_YELLOW "WARNING: %s protocol %i, but a ui module using the %s interface was found\n", + (clc.demoplaying ? "Demo was recorded using" : "Server uses"), + (clc.netchan.alternateProtocol == 0 ? PROTOCOL_VERSION : clc.netchan.alternateProtocol == 1 ? 70 : 69), + (cls.uiInterface == 2 ? "1.1" : "non-1.1")); + } + + // init for this gamestate + VM_Call(cls.ui, UI_INIT, (clc.state >= CA_AUTHORIZING && clc.state < CA_ACTIVE)); + + // show where the ui folder was loaded from + Cmd_ExecuteString("which ui/\n"); + + clc.newsString[0] = '\0'; +} + +/* +==================== +UI_GameCommand + +See if the current console command is claimed by the ui +==================== +*/ +bool UI_GameCommand(void) +{ + if (!cls.ui) return false; + + return (bool)VM_Call(cls.ui, UI_CONSOLE_COMMAND - (cls.uiInterface == 2 ? 2 : 0), cls.realtime); +} diff --git a/src/client/cl_updates.cpp b/src/client/cl_updates.cpp new file mode 100644 index 0000000..0e6a237 --- /dev/null +++ b/src/client/cl_updates.cpp @@ -0,0 +1,476 @@ +#include "cl_updates.h" + +#include <libgen.h> +#include <unistd.h> + +#include "nettle/rsa.h" +#include "nettle/sha2.h" +#include "rapidjson.h" +#include "restclient/connection.h" +#include "restclient/restclient.h" +#include "semantic_version.h" + +#include <iostream> +#include <fstream> +#include <vector> +#include <array> + +#include "qcommon/cvar.h" +#include "qcommon/q_platform.h" +#include "qcommon/q_shared.h" +#include "qcommon/qcommon.h" +#include "qcommon/unzip.h" +#include "sys/sys_shared.h" + +#include "cl_rest.h" + +using namespace std; + +std::vector<std::string> release_package; +static std::string granger_exe; +static std::string granger_main_lua; +static int nextCheckTime = 0; + +static const uint8_t release_key_pub[] = { + 0x28, 0x31, 0x30, 0x3a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x6b, + 0x65, 0x79, 0x28, 0x39, 0x3a, 0x72, 0x73, 0x61, 0x2d, 0x70, 0x6b, 0x63, + 0x73, 0x31, 0x28, 0x31, 0x3a, 0x6e, 0x35, 0x31, 0x33, 0x3a, 0x00, 0xaa, + 0x4a, 0xe1, 0xdc, 0x08, 0xed, 0x90, 0x92, 0x66, 0x4a, 0xc2, 0x00, 0x43, + 0x3e, 0x89, 0x40, 0x4d, 0x11, 0xaa, 0x98, 0x2d, 0x55, 0x08, 0xec, 0x43, + 0x9f, 0x73, 0xfe, 0xc9, 0x67, 0xcb, 0xb0, 0x4a, 0x52, 0x89, 0x1d, 0x1a, + 0xc1, 0x86, 0x29, 0x26, 0x32, 0xb2, 0x34, 0xb6, 0xa5, 0x42, 0x16, 0x08, + 0x60, 0x8d, 0xf1, 0xe4, 0x45, 0xd4, 0x90, 0x2b, 0xfb, 0x98, 0xb2, 0x2e, + 0xa7, 0xa9, 0x79, 0xff, 0x5e, 0xaa, 0xb5, 0xd6, 0xf9, 0xf9, 0x2e, 0x67, + 0x5c, 0xb6, 0x6c, 0x36, 0x70, 0xf3, 0xea, 0x3f, 0xd8, 0xc8, 0xd0, 0xda, + 0xd8, 0xfb, 0x1b, 0x55, 0xe9, 0x69, 0x9d, 0x4a, 0xea, 0xf2, 0xb1, 0xdd, + 0x6b, 0xea, 0xb4, 0x99, 0xd7, 0x5e, 0xca, 0x5f, 0x34, 0xcf, 0xee, 0xbb, + 0xc6, 0x07, 0xa8, 0x3b, 0x59, 0xc3, 0xc1, 0x53, 0x15, 0x2b, 0xe4, 0x2f, + 0x17, 0x2d, 0x0c, 0x0f, 0x4e, 0xee, 0x2e, 0xef, 0x97, 0x9c, 0xff, 0x58, + 0xec, 0x6a, 0xf1, 0x12, 0x21, 0x5a, 0xc5, 0xbb, 0x6a, 0xef, 0xb0, 0xc8, + 0x4b, 0x7d, 0x33, 0xca, 0x53, 0x03, 0x31, 0x4a, 0xbd, 0x82, 0x01, 0x56, + 0x8f, 0x4e, 0x1d, 0x21, 0x7d, 0x2e, 0x8d, 0xaa, 0x0d, 0xcd, 0xe6, 0x19, + 0x09, 0x79, 0x76, 0x11, 0x16, 0xb5, 0xd2, 0xc1, 0x03, 0xdb, 0xaa, 0x11, + 0xd5, 0x89, 0x70, 0xc8, 0x71, 0xbf, 0x1a, 0xea, 0x9d, 0xd6, 0xaa, 0x77, + 0x97, 0xa4, 0xcc, 0x1d, 0xd6, 0x1c, 0x1a, 0x14, 0x6f, 0x35, 0x34, 0x6a, + 0x10, 0x32, 0x13, 0x33, 0x41, 0x27, 0xad, 0x1b, 0x59, 0x41, 0xff, 0xb0, + 0x1b, 0x64, 0x14, 0xea, 0x29, 0x44, 0x2b, 0x62, 0xdc, 0x63, 0xbc, 0xc7, + 0xf8, 0xac, 0xf7, 0x5c, 0x9d, 0x3c, 0xbd, 0x17, 0x84, 0xd9, 0x56, 0x09, + 0x44, 0x1b, 0xdd, 0x72, 0x16, 0xf6, 0x67, 0xc4, 0x49, 0xd3, 0x56, 0x74, + 0x42, 0x2d, 0x08, 0xe2, 0x0b, 0x0c, 0x40, 0x97, 0x62, 0x97, 0xb3, 0xd6, + 0x0c, 0x69, 0x04, 0x6a, 0xd7, 0x7b, 0xf0, 0xe1, 0x37, 0x0b, 0xe0, 0xee, + 0x6b, 0x2d, 0x79, 0xa0, 0x72, 0xa5, 0x75, 0x97, 0xb1, 0x6b, 0x01, 0xa2, + 0xb8, 0xe3, 0xc7, 0x3c, 0x10, 0x50, 0x59, 0xe4, 0xda, 0x9e, 0x8d, 0xe6, + 0x1b, 0xef, 0xa6, 0xae, 0xc4, 0xd7, 0xd8, 0x9b, 0x57, 0x4c, 0xa3, 0xd7, + 0x9f, 0x37, 0x17, 0x72, 0x7f, 0x60, 0xd6, 0x4c, 0x42, 0x14, 0x54, 0x66, + 0x88, 0xc6, 0xa2, 0x1e, 0xdf, 0x55, 0xfc, 0xef, 0xb9, 0x05, 0x63, 0x8a, + 0xc4, 0x5b, 0xe7, 0x45, 0x28, 0x0e, 0x55, 0x8e, 0xcc, 0x74, 0x25, 0xf2, + 0xea, 0x2a, 0x66, 0x51, 0xf2, 0x8f, 0x72, 0x1d, 0x97, 0x5a, 0xfb, 0x1c, + 0x95, 0x01, 0x93, 0x6b, 0x2b, 0xa5, 0x87, 0x5d, 0xd8, 0xea, 0xb2, 0x24, + 0x78, 0xb1, 0x58, 0x63, 0x47, 0x86, 0xed, 0xd9, 0xbc, 0xe9, 0xd2, 0xea, + 0xa8, 0x90, 0x5b, 0xe8, 0x82, 0x89, 0xa2, 0xe2, 0x52, 0x1f, 0x78, 0x00, + 0x93, 0x64, 0x54, 0xdb, 0x9b, 0x93, 0xc3, 0xee, 0xa2, 0x37, 0xab, 0x2e, + 0x7e, 0x8f, 0xec, 0xb1, 0x0b, 0x69, 0xad, 0x21, 0xda, 0xa9, 0xaf, 0xd2, + 0x22, 0x52, 0x2f, 0x1a, 0x6b, 0xb9, 0x21, 0x5e, 0xe9, 0x1a, 0xe1, 0x4c, + 0x33, 0x26, 0x46, 0xa1, 0xde, 0x52, 0xf3, 0x87, 0xf4, 0x8c, 0x99, 0xe4, + 0x5d, 0xe7, 0x5b, 0x76, 0x6c, 0xf5, 0xe1, 0xae, 0x5b, 0xaa, 0xbb, 0x3a, + 0xf4, 0x90, 0xa7, 0x5c, 0x5c, 0x72, 0xab, 0xb8, 0x71, 0xdc, 0x47, 0xb1, + 0x75, 0x0d, 0xc8, 0xcc, 0xfd, 0xce, 0x62, 0xbe, 0xe5, 0x9f, 0x85, 0x00, + 0x53, 0x35, 0x91, 0x44, 0x29, 0x5f, 0x64, 0x8d, 0x7c, 0x37, 0x32, 0x28, + 0x45, 0x9d, 0xa3, 0x49, 0xbb, 0xe3, 0xd3, 0x1e, 0x0a, 0xae, 0x53, 0xbd, + 0x66, 0x86, 0x8c, 0x4d, 0xa6, 0xd3, 0x85, 0x29, 0x28, 0x31, 0x3a, 0x65, + 0x33, 0x3a, 0x01, 0x00, 0x01, 0x29, 0x29, 0x29 +}; +static const size_t release_key_pub_len = 560; + +struct UpdateManager { + + static void refresh(); + static void download(); + static void validate_signature(std::string path, std::string signature_path); + static void extract(std::string extract_path, std::string path); + static void execute(); + +private: + static constexpr auto url = "https://api.github.com/repos/GrangerHub/tremulous/releases"; + static constexpr auto package_name = RELEASE_PACKAGE_NAME; + static constexpr auto signature_name = RELEASE_SIGNATURE_NAME; + static constexpr auto granger_binary_name = GRANGER_EXE; +}; + +#define AU_ACT_NIL 0 +#define AU_ACT_GET 1 +#define AU_ACT_RUN 2 + +void UpdateManager::refresh() +{ + auto currentTime = Sys_Milliseconds(); + + if ( nextCheckTime > currentTime ) + return; + + nextCheckTime = currentTime + 10000; + + Cvar_SetValue("ui_autoupdate_action", 0); + Cvar_Set("cl_latestDownload", ""); + Cvar_Set("cl_latestRelease", ""); + Cvar_Set("cl_latestSignature", ""); + + RestClient::Response r = RestClient::get(url); + if ( r.code != 200 ) + { + std::string msg { va("Server did not return OK status code: %d", r.code) }; + + Cvar_Set("cl_latestRelease", + S_COLOR_RED "ERROR:\n" + S_COLOR_WHITE "Server did not return OK status code"); + + return; + } + + rapidjson::Document d; + d.Parse( r.body.c_str() ); + + rapidjson::Value &release = d[0]; + std::string txt; + + semver::v2::Version current { PRODUCT_VERSION + 1 }; + semver::v2::Version latest { release["tag_name"].GetString() + 1 }; + + if ( current == latest ) + { + txt += "You are up to date\n\n"; + } + else if ( current < latest ) + { + txt += "A new release is available!\n\n"; + } + else if ( current > latest ) + { + txt += "Wow! You are ahead of the release.\n\n"; + } + + txt += release["tag_name"].GetString(); + if (release["prerelease"].IsTrue()) + txt += S_COLOR_YELLOW " (Prerelease)"; + + txt += '\n'; + txt += S_COLOR_CYAN "Released:" S_COLOR_WHITE; + txt += release["published_at"].GetString(); + + txt += '\n'; + txt += S_COLOR_RED "Release Notes:\n" S_COLOR_WHITE; + + if (!release["body"].IsNull()) + { + txt += '\n'; + txt += release["body"].GetString(); + } + + rapidjson::Value &assets = release["assets"]; + for ( rapidjson::SizeType i = 0; i < assets.Size(); ++i ) + { + auto &a = assets[i]; + std::string name = a["name"].GetString(); + + if ( name == package_name ) + { + std::string dl = a["browser_download_url"].GetString(); + Cvar_Set("cl_latestDownload", dl.c_str()); + Cvar_Set("cl_latestPackage", package_name); + Cvar_Set("cl_latestRelease", txt.c_str()); + Cvar_SetValue("ui_autoupdate_action", AU_ACT_GET); + } + else if ( name == signature_name ) + { + std::string dl = a["browser_download_url"].GetString(); + Cvar_Set("cl_latestSignature", dl.c_str()); + } + } +} + +void UpdateManager::extract(std::string extract_path, std::string path) +{ + // Extract the release package + auto z = unzOpen(path.c_str()); + assert( z != nullptr ); + + unz_global_info zi; + unzGetGlobalInfo(z, &zi); + + // Iterate through all files in the package + unzGoToFirstFile(z); + for (int i = 0; i < zi.number_entry; ++i) + { + unz_file_info fi; + char filename[256] = ""; // 256 == MAX_ZPATH + int err; + + err = unzGetCurrentFileInfo(z, &fi, filename, sizeof(filename), + nullptr, 0, nullptr, 0); + assert( err == UNZ_OK ); // != OK means corrupt archive + + std::string fn = { filename }; + + // FIXME stop doing dumb string stuff + std::string fullpath = extract_path; + fullpath += PATH_SEP; + fullpath += fn; + release_package.emplace_back(fn); + + // Bad assumption of a directory? + if ( !fi.compressed_size && !fi.uncompressed_size ) + { + Com_DPrintf(S_COLOR_CYAN"ARCHIVED DIR: " + S_COLOR_WHITE"%s\n", + fn.c_str()); + + MakeDir(extract_path, fn); + + unzGoToNextFile(z); + continue; + } + + // Must be a file + Com_DPrintf(S_COLOR_CYAN"ARCHIVED FILE: " + S_COLOR_WHITE"%s (%lu/%lu)\n", + filename, + fi.compressed_size, + fi.uncompressed_size); + + if ( fn.rfind(granger_binary_name) != std::string::npos ) + granger_exe = fn; + + else if ( fn.rfind("main.lua") != std::string::npos ) + granger_main_lua = fn; + + err = unzOpenCurrentFile(z); + assert( err == UNZ_OK ); + + // FIXME cleanup all this shit string stuff + std::string path = extract_path; + path += PATH_SEP; + path += fn; + + Com_DPrintf(S_COLOR_YELLOW"Extracted FILE: " + S_COLOR_WHITE"%s\n", + path.c_str()); + + std::fstream dl; + dl.open(path, fstream::out|ios::binary); + + // Extract the release package + size_t numwrote; + do { + // Extract 16k at a time + unsigned blocksiz = 16384; + + if ( blocksiz > fi.uncompressed_size - numwrote ) + blocksiz = fi.uncompressed_size - numwrote; + + uint8_t block[blocksiz]; + unzReadCurrentFile(z, static_cast<void*>(&block), blocksiz); + + dl.write((const char*)block, blocksiz); + numwrote += blocksiz; + + } while ( fi.uncompressed_size > numwrote ); + dl.close(); + + unzCloseCurrentFile(z); + unzGoToNextFile(z); + } + + unzClose(z); +} + +void UpdateManager::download() +{ + cvar_t *cl_enableSignatureCheck = Cvar_Get("cl_enableSignatureCheck", "0", CVAR_ARCHIVE | CVAR_PROTECTED); + + // Check for and download signature + std::string signature_url = Cvar_VariableString("cl_latestSignature"); + if ( signature_url.empty() && cl_enableSignatureCheck->integer) + throw exception(); + + // Download the latest release + auto url = Cvar_VariableString("cl_latestDownload"); + auto r = RestClient::get(url); + if ( r.code != 200 ) + throw exception(); + + Com_DPrintf(S_COLOR_CYAN "URL: " S_COLOR_WHITE "%s\n", url); + + // FIXME Cleanup this string bullshit + std::string extract_path, signature_path, path { Cvar_VariableString("fs_homepath") }; + path += PATH_SEP; + extract_path = path; + signature_path = path + signature_name; + path += package_name; + + MakeDir(extract_path, "extract"); + extract_path += "extract"; + + Com_DPrintf(S_COLOR_CYAN"PATH: " S_COLOR_WHITE "%s\n", + path.c_str()); + + // Write the release package to disk + { + std::fstream dl; + dl.open( path, fstream::out|ios::binary ); + dl.write( r.body.c_str(), r.body.length() ); + dl.close(); + } + + if (cl_enableSignatureCheck->integer) + { + auto r = RestClient::get(signature_url); + if ( r.code != 200 ) + throw exception(); + + std::fstream dl; + dl.open( signature_path, fstream::out|ios::binary ); + dl.write( r.body.c_str(), r.body.length() ); + dl.close(); + + // Validate the signature of the package if enabled + UpdateManager::validate_signature(path, signature_path); + } + + // Extract the contents of the release package + UpdateManager::extract(extract_path, path); + + // Delete the release package + unlink(path.c_str()); + + Cvar_SetValue("au_autoupdate_action", AU_ACT_RUN); +} + +void UpdateManager::validate_signature(std::string path, std::string signature_path) +{ + // Load public key + rsa_public_key public_key; + rsa_public_key_init(&public_key); + rsa_keypair_from_sexp(&public_key, NULL, 0, release_key_pub_len, release_key_pub); + + auto min = [](auto a, auto b) { return a < b ? a : b; }; + + // Read in signature + mpz_t signature; + { + std::ifstream f(signature_path, ios::binary); + f.seekg (0, f.end); + size_t length = f.tellg(); + f.seekg (0, f.beg); + + std::vector<char> buffer(512, 0); + f.read(buffer.data(), min(length, buffer.size())); + nettle_mpz_init_set_str_256_u(signature, f.gcount(), (uint8_t *)buffer.data()); + } + + // Hash file + sha256_ctx ctx; + sha256_init(&ctx); + { + std::ifstream f(path, ios::binary); + f.seekg (0, f.end); + size_t length = f.tellg(); + f.seekg (0, f.beg); + + std::vector<unsigned char> buffer(16384, 0); + while (f.read((char *)buffer.data(), min(length, buffer.size()))) { + auto nbytes = f.gcount(); + sha256_update(&ctx, nbytes, buffer.data()); + length -= nbytes; + if (length <= 0) { + break; + } + } + } + + // Verify signature + if (!rsa_sha256_verify(&public_key, &ctx, signature)) { + rsa_public_key_clear(&public_key); + mpz_clear(signature); + Com_Error( ERR_DROP, "Update signature was not verified\n" ); + return; + } + + unlink(signature_path.c_str()); + rsa_public_key_clear(&public_key); + mpz_clear(signature); +} + +extern char** environ; + +class FailInstaller : public std::exception { + std::string msg; +public: + FailInstaller(int e) + { msg = strerror(e); } + + virtual const char* what() throw() + { return msg.c_str(); } +}; + +void UpdateManager::execute() +{ + granger_exe = ""; + granger_main_lua = ""; + + if ( granger_exe == "" || granger_main_lua == "" ) + { + for ( auto const& i : release_package ) + { + Com_Printf(S_COLOR_RED " Unlink %s\n", i.c_str()); + unlink(i.c_str()); + } + + Com_Error( ERR_DROP, "Missing Granger or GrangerScript\n" ); + return; + } + + std::array<const char*, 4> argv{}; + + char* tmp = strdup(granger_exe.c_str()); + const char* dir = dirname(tmp); + + argv[0] = granger_exe.c_str(); + argv[1] = va(" -C %s", dir); + argv[2] = granger_main_lua.c_str(); + + // Dump the details + Com_Printf(S_COLOR_YELLOW"Launching " + S_COLOR_WHITE"%s %s %s\n", + argv[0], argv[1], argv[2]); + +#ifndef _WIN32 + // Fork solely to try cleanup SDL2/GL states + auto pid = fork(); + if (pid == -1) + throw FailInstaller(errno); + + if (pid == 0) + { + execve(argv[0], + const_cast<char **>(argv.data()), + environ); + + throw FailInstaller(errno); + } + else + { + Engine_Exit(""); + } +#else + // FIXME Dirty exit on Windows... + execve(argv[0], + const_cast<char **>(argv.data()), + environ); + + throw FailInstaller(errno); +#endif +} + +void CL_GetLatestRelease() { UpdateManager::refresh(); } +void CL_DownloadRelease() { UpdateManager::download(); } +void CL_ExecuteInstaller() { UpdateManager::execute(); } diff --git a/src/client/cl_updates.h b/src/client/cl_updates.h new file mode 100644 index 0000000..f85c64a --- /dev/null +++ b/src/client/cl_updates.h @@ -0,0 +1,8 @@ +#ifndef CL_UPDATES +#define CL_UPDATES + +void CL_GetLatestRelease(); +void CL_DownloadRelease(); +void CL_ExecuteInstaller(); + +#endif diff --git a/src/client/client.h b/src/client/client.h new file mode 100644 index 0000000..2474a3d --- /dev/null +++ b/src/client/client.h @@ -0,0 +1,716 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ +// client.h -- primary header for client + +#ifndef _CLIENT_H_ +#define _CLIENT_H_ + +#ifdef USE_VOIP +#include <opus.h> +#endif + +#include "cgame/cg_public.h" +#include "qcommon/alternatePlayerstate.h" +#include "qcommon/cmd.h" +#include "qcommon/crypto.h" +#include "qcommon/cvar.h" +#include "qcommon/files.h" +#include "qcommon/huffman.h" +#include "qcommon/msg.h" +#include "qcommon/net.h" +#include "qcommon/q_shared.h" +#include "qcommon/qcommon.h" +#include "qcommon/vm.h" +#include "renderercommon/tr_public.h" +#include "sys/sys_shared.h" +#include "ui/ui_public.h" + +#include "cl_curl.h" +#include "keys.h" +#include "snd_public.h" + +struct alternateEntityState_t { + int number; // entity index + int eType; // entityType_t + int eFlags; + + trajectory_t pos; // for calculating position + trajectory_t apos; // for calculating angles + + int time; + int time2; + + vec3_t origin; + vec3_t origin2; + + vec3_t angles; + vec3_t angles2; + + int otherEntityNum; // shotgun sources, etc + int otherEntityNum2; + + int groundEntityNum; // ENTITYNUM_NONE = in air + + int constantLight; // r + (g<<8) + (b<<16) + (intensity<<24) + int loopSound; // constantly loop this sound + + int modelindex; + int modelindex2; + int clientNum; // 0 to (MAX_CLIENTS - 1), for players and corpses + int frame; + + int solid; // for client side prediction, trap_linkentity sets this properly + + int event; // impulse events -- muzzle flashes, footsteps, etc + int eventParm; + + // for players + int misc; // bit flags + int weapon; // determines weapon and flash model, etc + int legsAnim; // mask off ANIM_TOGGLEBIT + int torsoAnim; // mask off ANIM_TOGGLEBIT + + int generic1; +}; + +struct alternateSnapshot_t { + int snapFlags; // SNAPFLAG_RATE_DELAYED, etc + int ping; + + int serverTime; // server time the message is valid for (in msec) + + byte areamask[MAX_MAP_AREA_BYTES]; // portalarea visibility bits + + alternatePlayerState_t ps; // complete information about the current player at this time + + int numEntities; // all of the entities that need to be presented + alternateEntityState_t entities[MAX_ENTITIES_IN_SNAPSHOT]; // at the time of this snapshot + + int numServerCommands; // text based server commands to execute when this + int serverCommandSequence; // snapshot becomes current +}; + +// file full of random crap that gets used to create cl_guid +#define QKEY_FILE "qkey" +#define QKEY_SIZE 2048 + +#define RETRANSMIT_TIMEOUT 3000 // time between connection packet retransmits + +// snapshots are a view of the server at a given time +struct clSnapshot_t { + bool valid; // cleared if delta parsing was invalid + int snapFlags; // rate delayed and dropped commands + + int serverTime; // server time the message is valid for (in msec) + + int messageNum; // copied from netchan->incoming_sequence + int deltaNum; // messageNum the delta is from + int ping; // time from when cmdNum-1 was sent to time packet was reeceived + byte areamask[MAX_MAP_AREA_BYTES]; // portalarea visibility bits + + int cmdNum; // the next cmdNum the server is expecting + playerState_t ps; // complete information about the current player at this time + alternatePlayerState_t alternatePs; // complete information about the current player at this time + + int numEntities; // all of the entities that need to be presented + int parseEntitiesNum; // at the time of this snapshot + + int serverCommandNum; // execute all commands up to this before + // making the snapshot current +}; + +/* +============================================================================= + +the clientActive_t structure is wiped completely at every +new gamestate_t, potentially several times during an established connection + +============================================================================= +*/ + +struct outPacket_t { + int p_cmdNumber; // cl.cmdNumber when packet was sent + int p_serverTime; // usercmd->serverTime when packet was sent + int p_realtime; // cls.realtime when packet was sent +}; + +// the parseEntities array must be large enough to hold PACKET_BACKUP frames of +// entities, so that when a delta compressed message arives from the server +// it can be un-deltad from the original +#define MAX_PARSE_ENTITIES (PACKET_BACKUP * MAX_SNAPSHOT_ENTITIES) + +extern int g_console_field_width; + +struct clientActive_t { + int timeoutcount; // it requres several frames in a timeout condition + // to disconnect, preventing debugging breaks from + // causing immediate disconnects on continue + clSnapshot_t snap; // latest received from server + + int serverTime; // may be paused during play + int oldServerTime; // to prevent time from flowing bakcwards + int oldFrameServerTime; // to check tournament restarts + int serverTimeDelta; // cl.serverTime = cls.realtime + cl.serverTimeDelta + // this value changes as net lag varies + bool extrapolatedSnapshot; // set if any cgame frame has been forced to extrapolate + // cleared when CL_AdjustTimeDelta looks at it + bool newSnapshots; // set on parse of any valid packet + + gameState_t gameState; // configstrings + char mapname[MAX_QPATH]; // extracted from CS_SERVERINFO + + int parseEntitiesNum; // index (not anded off) into cl_parse_entities[] + + int mouseDx[2], mouseDy[2]; // added to by mouse events + int mouseIndex; + int joystickAxis[MAX_JOYSTICK_AXIS]; // set by joystick events + + // cgame communicates a few values to the client system + int cgameUserCmdValue; // current weapon to add to usercmd_t + float cgameSensitivity; + + // cmds[cmdNumber] is the predicted command, [cmdNumber-1] is the last + // properly generated command + usercmd_t cmds[CMD_BACKUP]; // each mesage will send several old cmds + int cmdNumber; // incremented each frame, because multiple + // frames may need to be packed into a single packet + + outPacket_t outPackets[PACKET_BACKUP]; // information about each packet we have sent out + + // the client maintains its own idea of view angles, which are + // sent to the server each frame. It is cleared to 0 upon entering each level. + // the server sends a delta each frame which is added to the locally + // tracked view angles to account for standing on rotating objects, + // and teleport direction changes + vec3_t viewangles; + + int serverId; // included in each client message so the server + // can tell if it is for a prior map_restart + // big stuff at end of structure so most offsets are 15 bits or less + clSnapshot_t snapshots[PACKET_BACKUP]; + + entityState_t entityBaselines[MAX_GENTITIES]; // for delta compression when not in previous frame + + entityState_t parseEntities[MAX_PARSE_ENTITIES]; +}; + +extern clientActive_t cl; + +/* +============================================================================= + +the clientConnection_t structure is wiped when disconnecting from a server, +either to go to a full screen console, play a demo, or connect to a different server + +A connection can be to either a server through the network layer or a +demo through a file. + +============================================================================= +*/ + +#define MAX_TIMEDEMO_DURATIONS 4096 + +struct clientConnection_t { + connstate_t state; // connection status + + int clientNum; + int lastPacketSentTime; // for retransmits during connection + int lastPacketTime; // for timeouts + + char servername[MAX_OSPATH]; // name of server from original connect (used by reconnect) + netadr_t serverAddress; + int connectTime; // for connection retransmits + int connectPacketCount; // for display on connection dialog + char serverMessage[MAX_STRING_TOKENS]; // for display on connection dialog + + int challenge; // from the server to use for connecting + char challenge2[33]; + bool sendSignature; + int checksumFeed; // from the server for checksum calculations + + // these are our reliable messages that go to the server + int reliableSequence; + int reliableAcknowledge; // the last one the server has executed + char reliableCommands[MAX_RELIABLE_COMMANDS][MAX_STRING_CHARS]; + + // server message (unreliable) and command (reliable) sequence + // numbers are NOT cleared at level changes, but continue to + // increase as long as the connection is valid + + // message sequence is used by both the network layer and the + // delta compression layer + int serverMessageSequence; + + // reliable messages received from server + int serverCommandSequence; + int lastExecutedServerCommand; // last server command grabbed or executed with CL_GetServerCommand + char serverCommands[MAX_RELIABLE_COMMANDS][MAX_STRING_CHARS]; + + // file transfer from server + fileHandle_t download; + char downloadTempName[MAX_OSPATH]; + char downloadName[MAX_OSPATH]; + + // XXX Refactor this -vjr + bool cURLEnabled; + bool cURLUsed; + bool cURLDisconnected; + + char downloadURL[MAX_OSPATH]; + CURL *downloadCURL; + CURLM *downloadCURLM; + bool activeCURLNotGameRelated; + + int sv_allowDownload; + char sv_dlURL[MAX_CVAR_VALUE_STRING]; + int downloadNumber; + int downloadBlock; // block we are waiting for + int downloadCount; // how many bytes we got + int downloadSize; // how many bytes we got + char downloadList[MAX_INFO_STRING]; // list of paks we need to download + bool downloadRestart; // if true, we need to do another FS_Restart because we downloaded a pak + char newsString[MAX_NEWS_STRING]; + + // demo information + char demoName[MAX_QPATH]; + bool spDemoRecording; + bool demorecording; + bool demoplaying; + bool demowaiting; // don't record until a non-delta message is received + bool firstDemoFrameSkipped; + fileHandle_t demofile; + + int timeDemoFrames; // counter of rendered frames + int timeDemoStart; // cls.realtime before first frame + int timeDemoBaseTime; // each frame will be at this time + frameNum * 50 + int timeDemoLastFrame; // time the last frame was rendered + int timeDemoMinDuration; // minimum frame duration + int timeDemoMaxDuration; // maximum frame duration + unsigned char timeDemoDurations[MAX_TIMEDEMO_DURATIONS]; // log of frame durations + + float aviVideoFrameRemainder; + float aviSoundFrameRemainder; + +#ifdef USE_VOIP + bool voipEnabled; + bool voipCodecInitialized; + + // incoming data... + // !!! FIXME: convert from parallel arrays to array of a struct. + OpusDecoder *opusDecoder[MAX_CLIENTS]; + byte voipIncomingGeneration[MAX_CLIENTS]; + int voipIncomingSequence[MAX_CLIENTS]; + float voipGain[MAX_CLIENTS]; + bool voipIgnore[MAX_CLIENTS]; + bool voipMuteAll; + + // outgoing data... + // if voipTargets[i / 8] & (1 << (i % 8)), + // then we are sending to clientnum i. + uint8_t voipTargets[(MAX_CLIENTS + 7) / 8]; + uint8_t voipFlags; + OpusEncoder *opusEncoder; + int voipOutgoingDataSize; + int voipOutgoingDataFrames; + int voipOutgoingSequence; + byte voipOutgoingGeneration; + byte voipOutgoingData[1024]; + float voipPower; +#endif + + // big stuff at end of structure so most offsets are 15 bits or less + netchan_t netchan; +}; + +extern clientConnection_t clc; + +/* +================================================================== + +the clientStatic_t structure is never wiped, and is used even when +no client connection is active at all +(except when CL_Shutdown is called) + +================================================================== +*/ + +struct ping_t { + netadr_t adr; + int start; + int time; + char info[MAX_INFO_STRING]; +}; + +#define MAX_FEATLABEL_CHARS 32 +struct serverInfo_t { + netadr_t adr; + char hostName[MAX_HOSTNAME_LENGTH]; + char mapName[MAX_NAME_LENGTH]; + char game[MAX_NAME_LENGTH]; + char label[MAX_FEATLABEL_CHARS]; // for featured servers, NULL otherwise + int netType; + int gameType; + int clients; + int maxClients; + int minPing; + int maxPing; + int ping; + bool visible; +}; + +struct clientStatic_t { + // when the server clears the hunk, all of these must be restarted + bool rendererStarted; + bool soundStarted; + bool soundRegistered; + bool uiStarted; + bool cgameStarted; + + int framecount; + int frametime; // msec since last frame + + int realtime; // ignores pause + int realFrametime; // ignoring pause, so console always works + + // master server sequence information + int numAlternateMasterPackets[3]; + unsigned int receivedAlternateMasterPackets[3]; // bitfield + + int numlocalservers; + serverInfo_t localServers[MAX_OTHER_SERVERS]; + + int numglobalservers; + serverInfo_t globalServers[MAX_GLOBAL_SERVERS]; + // additional global servers + int numGlobalServerAddresses; + netadr_t globalServerAddresses[MAX_GLOBAL_SERVERS]; + + int numfavoriteservers; + serverInfo_t favoriteServers[MAX_OTHER_SERVERS]; + + int pingUpdateSource; // source currently pinging or updating + + // update server info + netadr_t updateServer; + char updateChallenge[MAX_TOKEN_CHARS]; + char updateInfoString[MAX_INFO_STRING]; + + netadr_t authorizeServer; + + // rendering info + glconfig_t glconfig; + qhandle_t charSetShader; + qhandle_t whiteShader; + qhandle_t consoleShader; + + vm_t *cgame; + int cgInterface; // 0 == gpp, 2 == 1.1.0 + + vm_t *ui; + int uiInterface; + + struct { + struct rsa_public_key public_key; + struct rsa_private_key private_key; + } rsa; +}; + +extern clientStatic_t cls; + +extern char cl_oldGame[MAX_QPATH]; +extern bool cl_oldGameSet; + +//============================================================================= + +extern refexport_t re; // interface to refresh .dll + +// +// cvars +// +extern cvar_t *cl_nodelta; +extern cvar_t *cl_debugMove; +extern cvar_t *cl_noprint; +extern cvar_t *cl_timegraph; +extern cvar_t *cl_maxpackets; +extern cvar_t *cl_packetdup; +extern cvar_t *cl_shownet; +extern cvar_t *cl_showSend; +extern cvar_t *cl_timeNudge; +extern cvar_t *cl_showTimeDelta; +extern cvar_t *cl_freezeDemo; + +extern cvar_t *cl_yawspeed; +extern cvar_t *cl_pitchspeed; +extern cvar_t *cl_run; +extern cvar_t *cl_anglespeedkey; + +extern cvar_t *cl_sensitivity; +extern cvar_t *cl_freelook; + +extern cvar_t *cl_mouseAccel; +extern cvar_t *cl_mouseAccelOffset; +extern cvar_t *cl_mouseAccelStyle; +extern cvar_t *cl_showMouseRate; + +extern cvar_t *m_pitch; +extern cvar_t *m_yaw; +extern cvar_t *m_forward; +extern cvar_t *m_side; +extern cvar_t *m_filter; + +extern cvar_t *j_pitch; +extern cvar_t *j_yaw; +extern cvar_t *j_forward; +extern cvar_t *j_side; +extern cvar_t *j_up; +extern cvar_t *j_pitch_axis; +extern cvar_t *j_yaw_axis; +extern cvar_t *j_forward_axis; +extern cvar_t *j_side_axis; +extern cvar_t *j_up_axis; + +extern cvar_t *cl_timedemo; +extern cvar_t *cl_aviFrameRate; +extern cvar_t *cl_aviMotionJpeg; + +extern cvar_t *cl_activeAction; + +extern cvar_t *cl_allowDownload; +extern cvar_t *cl_downloadMethod; +extern cvar_t *cl_conXOffset; +extern cvar_t *cl_inGameVideo; + +extern cvar_t *cl_lanForcePackets; +extern cvar_t *cl_autoRecordDemo; + +extern cvar_t *cl_clantag; + +extern cvar_t *cl_consoleKeys; + +#ifdef USE_MUMBLE +extern cvar_t *cl_useMumble; +extern cvar_t *cl_mumbleScale; +#endif + +#ifdef USE_VOIP +// cl_voipSendTarget is a string: "all" to broadcast to everyone, "none" to +// send to no one, or a comma-separated list of client numbers: +// "0,7,2,23" ... an empty string is treated like "all". +extern cvar_t *cl_voipUseVAD; +extern cvar_t *cl_voipVADThreshold; +extern cvar_t *cl_voipSend; +extern cvar_t *cl_voipSendTarget; +extern cvar_t *cl_voipGainDuringCapture; +extern cvar_t *cl_voipCaptureMult; +extern cvar_t *cl_voipShowMeter; +extern cvar_t *cl_voip; + +// 20ms at 48k +#define VOIP_MAX_FRAME_SAMPLES (20 * 48) + +// 3 frame is 60ms of audio, the max opus will encode at once +#define VOIP_MAX_PACKET_FRAMES 3 +#define VOIP_MAX_PACKET_SAMPLES (VOIP_MAX_FRAME_SAMPLES * VOIP_MAX_PACKET_FRAMES) +#endif + +extern cvar_t *cl_rsaAuth; + +//================================================= + +// +// cl_main +// + +void CL_Init(void); +void CL_AddReliableCommand(const char *cmd, bool isDisconnectCmd); + +void CL_StartHunkUsers(bool rendererOnly); + +void CL_Disconnect_f(void); +void CL_NextDemo(void); +void CL_ReadDemoMessage(void); +demoState_t CL_DemoState(void); +int CL_DemoPos(void); +void CL_DemoName(char *buffer, int size); +void CL_StopRecord_f(void); + +void CL_InitDownloads(void); +void CL_NextDownload(void); + +void CL_GetPing(int n, char *buf, int buflen, int *pingtime); +void CL_GetPingInfo(int n, char *buf, int buflen); +void CL_ClearPing(int n); +int CL_GetPingQueueCount(void); + +bool CL_ServerStatus(char *serverAddress, char *serverStatusString, int maxLen); + +bool CL_CheckPaused(void); + +// +// cl_input +// +typedef struct { + int down[2]; // key nums holding it down + unsigned downtime; // msec timestamp + unsigned msec; // msec down this frame if both a down and up happened + bool active; // current state + bool wasPressed; // set when down, not cleared when up +} kbutton_t; + +void CL_InitInput(void); +void CL_ShutdownInput(void); +void CL_SendCmd(void); +void CL_ClearState(void); +void CL_ReadPackets(void); + +void CL_WritePacket(void); +//void IN_CenterView(void); + +int Key_StringToKeynum(const char *str); +const char *Key_KeynumToString(int keynum); + +// +// cl_parse.c +// +extern bool cl_connectedToPureServer; +extern bool cl_connectedToCheatServer; + +#ifdef USE_VOIP +void CL_Voip_f(void); +#endif + +void CL_SystemInfoChanged(void); +void CL_ParseServerMessage(msg_t *msg); + +//==================================================================== + +bool CL_UpdateVisiblePings_f(int source); + +// +// console +// +void Con_DrawCharacter(int cx, int line, int num); + +void Con_CheckResize(void); +void Con_MessageModesInit(void); +void CL_ProtocolSpecificCommandsInit(void); +void Con_Init(void); +void Con_Shutdown(void); +void Con_Clear_f(void); +void Con_ToggleConsole_f(void); +void Con_ClearNotify(void); +void Con_RunConsole(void); +void Con_DrawConsole(void); +void Con_PageUp(void); +void Con_PageDown(void); +void Con_Top(void); +void Con_Bottom(void); +void Con_Close(void); + +void CL_LoadConsoleHistory(void); +void CL_SaveConsoleHistory(void); + +// +// cl_scrn.c +// +void SCR_Init(void); +void SCR_UpdateScreen(void); + +void SCR_DebugGraph(float value); + +int SCR_GetBigStringWidth(const char *str); // returns in virtual 640x480 coordinates + +void SCR_AdjustFrom640(float *x, float *y, float *w, float *h); +void SCR_FillRect(float x, float y, float width, float height, const float *color); +void SCR_DrawPic(float x, float y, float width, float height, qhandle_t hShader); +void SCR_DrawNamedPic(float x, float y, float width, float height, const char *picname); + +void SCR_DrawBigString(int x, int y, const char *s, float alpha, + bool noColorEscape); // draws a string with embedded color control characters with fade +void SCR_DrawBigStringColor( + int x, int y, const char *s, vec4_t color, bool noColorEscape); // ignores embedded color control characters +void SCR_DrawSmallStringExt(int x, int y, const char *string, float *setColor, bool forceColor, bool noColorEscape); +void SCR_DrawSmallChar(int x, int y, int ch); + +// +// cl_cin.c +// + +void CL_PlayCinematic_f(void); +void SCR_DrawCinematic(void); +void SCR_RunCinematic(void); +void SCR_StopCinematic(void); +int CIN_PlayCinematic(const char *arg0, int xpos, int ypos, int width, int height, int bits); +e_status CIN_StopCinematic(int handle); +e_status CIN_RunCinematic(int handle); +void CIN_DrawCinematic(int handle); +void CIN_SetExtents(int handle, int x, int y, int w, int h); +void CIN_UploadCinematic(int handle); +void CIN_CloseAllVideos(void); + +// +// cl_cgame.c +// +void CL_InitCGame(void); +void CL_ShutdownCGame(void); +bool CL_GameCommand(void); +void CL_GameConsoleText(void); +void CL_CGameRendering(stereoFrame_t stereo); +void CL_SetCGameTime(void); +void CL_FirstSnapshot(void); +void CL_ShaderStateChanged(void); + +// +// cl_ui.c +// +void CL_InitUI(void); +void CL_ShutdownUI(void); +int Key_GetCatcher(void); +void Key_SetCatcher(int catcher); +void LAN_LoadCachedServers(void); +void LAN_SaveServersToCache(void); + +// +// cl_net_chan.c +// +void CL_Netchan_Transmit(netchan_t *chan, msg_t *msg); // int length, const byte *data ); +bool CL_Netchan_Process(netchan_t *chan, msg_t *msg); + +// +// cl_avi.c +// +bool CL_OpenAVIForWriting(const char *filename); +void CL_TakeVideoFrame(void); +void CL_WriteAVIVideoFrame(const byte *imageBuffer, int size); +void CL_WriteAVIAudioFrame(const byte *pcmBuffer, int size); +bool CL_CloseAVI(void); +bool CL_VideoRecording(void); + +// +// cl_main.c +// +void CL_WriteDemoMessage(msg_t *msg, int headerBytes); +int CL_ScaledMilliseconds(void); + +#endif diff --git a/src/client/keycodes.h b/src/client/keycodes.h index ae6f189..3b2958d 100644 --- a/src/client/keycodes.h +++ b/src/client/keycodes.h @@ -1,13 +1,14 @@ /* =========================================================================== Copyright (C) 1999-2005 Id Software, Inc. -Copyright (C) 2000-2006 Tim Angus +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub This file is part of Tremulous. Tremulous is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as -published by the Free Software Foundation; either version 2 of the License, +published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. Tremulous is distributed in the hope that it will be @@ -16,8 +17,8 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License -along with Tremulous; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + =========================================================================== */ // @@ -262,6 +263,39 @@ typedef enum { K_EURO, K_UNDO, + // Gamepad controls + // Ordered to match SDL2 game controller buttons and axes + // Do not change this order without also changing IN_GamepadMove() in SDL_input.c + K_PAD0_A, + K_PAD0_B, + K_PAD0_X, + K_PAD0_Y, + K_PAD0_BACK, + K_PAD0_GUIDE, + K_PAD0_START, + K_PAD0_LEFTSTICK_CLICK, + K_PAD0_RIGHTSTICK_CLICK, + K_PAD0_LEFTSHOULDER, + K_PAD0_RIGHTSHOULDER, + K_PAD0_DPAD_UP, + K_PAD0_DPAD_DOWN, + K_PAD0_DPAD_LEFT, + K_PAD0_DPAD_RIGHT, + + K_PAD0_LEFTSTICK_LEFT, + K_PAD0_LEFTSTICK_RIGHT, + K_PAD0_LEFTSTICK_UP, + K_PAD0_LEFTSTICK_DOWN, + K_PAD0_RIGHTSTICK_LEFT, + K_PAD0_RIGHTSTICK_RIGHT, + K_PAD0_RIGHTSTICK_UP, + K_PAD0_RIGHTSTICK_DOWN, + K_PAD0_LEFTTRIGGER, + K_PAD0_RIGHTTRIGGER, + + // Pseudo-key that brings the console down + K_CONSOLE, + MAX_KEYS } keyNum_t; diff --git a/src/client/keys.h b/src/client/keys.h new file mode 100644 index 0000000..5c8877f --- /dev/null +++ b/src/client/keys.h @@ -0,0 +1,66 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ +#ifndef _KEYS_H_ +#define _KEYS_H_ + +#include "keycodes.h" + +typedef struct { + bool down; + int repeats; // if > 1, it is autorepeating + char *binding; +} qkey_t; + +extern bool key_overstrikeMode; +extern qkey_t keys[MAX_KEYS]; + +// NOTE TTimo the declaration of field_t and Field_Clear is now in qcommon/qcommon.h +void Field_KeyDownEvent(field_t *edit, int key); +void Field_CharEvent(field_t *edit, int ch); +void Field_Draw(field_t *edit, int x, int y, int width, bool showCursor, bool noColorEscape); +void Field_BigDraw(field_t *edit, int x, int y, int width, bool showCursor, bool noColorEscape); + +#define COMMAND_HISTORY 32 +extern field_t historyEditLines[COMMAND_HISTORY]; + +extern field_t g_consoleField; +extern field_t chatField; +extern int anykeydown; +extern bool chat_team; +extern bool chat_admins; +extern bool chat_clans; +extern int chat_playerNum; + +void Key_WriteBindings(fileHandle_t f); +void Key_SetBinding(int keynum, const char *binding); +const char *Key_GetBinding(int keynum); +bool Key_IsDown(int keynum); +bool Key_GetOverstrikeMode(void); +void Key_SetOverstrikeMode(bool state); +void Key_ClearStates(void); +int Key_GetKey(const char *binding); +void Key_KeynumToStringBuf(int keynum, char *buf, int buflen); +void Key_GetBindingBuf(int keynum, char *buf, int buflen); + +#endif diff --git a/src/client/libmumblelink.cpp b/src/client/libmumblelink.cpp new file mode 100644 index 0000000..59d59d3 --- /dev/null +++ b/src/client/libmumblelink.cpp @@ -0,0 +1,190 @@ +/* libmumblelink.c -- mumble link interface + + Copyright (C) 2008 Ludwig Nussel <ludwig.nussel@suse.de> + Copyright (C) 2000-2013 Darklegion Development + Copyright (C) 2015-2019 GrangerHub + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +*/ + +#include "libmumblelink.h" + +#include <fcntl.h> +#ifdef _WIN32 +#include <windows.h> +#define uint32_t UINT32 +#else +#include <unistd.h> +#ifdef __sun +#define _POSIX_C_SOURCE 199309L +#endif +#include <sys/mman.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/types.h> +#endif + +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> + +#ifndef MIN +#define MIN(a, b) ((a)<(b)?(a):(b)) +#endif + +typedef struct +{ + uint32_t uiVersion; + uint32_t uiTick; + float fAvatarPosition[3]; + float fAvatarFront[3]; + float fAvatarTop[3]; + wchar_t name[256]; + /* new in mumble 1.2 */ + float fCameraPosition[3]; + float fCameraFront[3]; + float fCameraTop[3]; + wchar_t identity[256]; + uint32_t context_len; + unsigned char context[256]; + wchar_t description[2048]; +} LinkedMem; + +static LinkedMem *lm = NULL; + +#ifdef WIN32 +static HANDLE hMapObject = NULL; +#else +static int32_t GetTickCount(void) +{ + struct timeval tv; + gettimeofday(&tv,NULL); + + return tv.tv_usec / 1000 + tv.tv_sec * 1000; +} +#endif + +int mumble_link(const char* name) +{ +#ifdef WIN32 + if(lm) + return 0; + + hMapObject = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, L"MumbleLink"); + if (hMapObject == NULL) + return -1; + + lm = (LinkedMem *) MapViewOfFile(hMapObject, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(LinkedMem)); + if (lm == NULL) { + CloseHandle(hMapObject); + hMapObject = NULL; + return -1; + } +#else + char file[256]; + int shmfd; + if(lm) + return 0; + + snprintf(file, sizeof (file), "/MumbleLink.%d", getuid()); + shmfd = shm_open(file, O_RDWR, S_IRUSR | S_IWUSR); + if(shmfd < 0) { + return -1; + } + + lm = (LinkedMem *) (mmap(NULL, sizeof(LinkedMem), PROT_READ | PROT_WRITE, MAP_SHARED, shmfd,0)); + if (lm == (void *) (-1)) { + lm = NULL; + close(shmfd); + return -1; + } + close(shmfd); +#endif + memset(lm, 0, sizeof(LinkedMem)); + mbstowcs(lm->name, name, sizeof(lm->name) / sizeof(wchar_t)); + + return 0; +} + +void mumble_update_coordinates(float fPosition[3], float fFront[3], float fTop[3]) +{ + mumble_update_coordinates2(fPosition, fFront, fTop, fPosition, fFront, fTop); +} + +void mumble_update_coordinates2(float fAvatarPosition[3], float fAvatarFront[3], float fAvatarTop[3], + float fCameraPosition[3], float fCameraFront[3], float fCameraTop[3]) +{ + if (!lm) + return; + + memcpy(lm->fAvatarPosition, fAvatarPosition, sizeof(lm->fAvatarPosition)); + memcpy(lm->fAvatarFront, fAvatarFront, sizeof(lm->fAvatarFront)); + memcpy(lm->fAvatarTop, fAvatarTop, sizeof(lm->fAvatarTop)); + memcpy(lm->fCameraPosition, fCameraPosition, sizeof(lm->fCameraPosition)); + memcpy(lm->fCameraFront, fCameraFront, sizeof(lm->fCameraFront)); + memcpy(lm->fCameraTop, fCameraTop, sizeof(lm->fCameraTop)); + lm->uiVersion = 2; + lm->uiTick = GetTickCount(); +} + +void mumble_set_identity(const char* identity) +{ + size_t len; + if (!lm) + return; + len = MIN(sizeof(lm->identity)/sizeof(wchar_t), strlen(identity)+1); + mbstowcs(lm->identity, identity, len); +} + +void mumble_set_context(const unsigned char* context, size_t len) +{ + if (!lm) + return; + len = MIN(sizeof(lm->context), len); + lm->context_len = len; + memcpy(lm->context, context, len); +} + +void mumble_set_description(const char* description) +{ + size_t len; + if (!lm) + return; + len = MIN(sizeof(lm->description)/sizeof(wchar_t), strlen(description)+1); + mbstowcs(lm->description, description, len); +} + +void mumble_unlink() +{ + if(!lm) + return; +#ifdef WIN32 + UnmapViewOfFile(lm); + CloseHandle(hMapObject); + hMapObject = NULL; +#else + munmap(lm, sizeof(LinkedMem)); +#endif + lm = NULL; +} + +int mumble_islinked(void) +{ + return lm != NULL; +} diff --git a/src/client/libmumblelink.h b/src/client/libmumblelink.h new file mode 100644 index 0000000..aa8e2ce --- /dev/null +++ b/src/client/libmumblelink.h @@ -0,0 +1,41 @@ +/* libmumblelink.h -- mumble link interface + + Copyright (C) 2008 Ludwig Nussel <ludwig.nussel@suse.de> + Copyright (C) 2000-2013 Darklegion Development + Copyright (C) 2015-2019 GrangerHub + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +*/ +#ifndef _LIBMUMBLELINK_H_ +#define _LIBMUMBLELINK_H_ + +int mumble_link(const char* name); +int mumble_islinked(void); +void mumble_update_coordinates(float fPosition[3], float fFront[3], float fTop[3]); + +/* new for mumble 1.2: also set camera position */ +void mumble_update_coordinates2(float fAvatarPosition[3], float fAvatarFront[3], float fAvatarTop[3], + float fCameraPosition[3], float fCameraFront[3], float fCameraTop[3]); + +void mumble_set_description(const char* description); +void mumble_set_context(const unsigned char* context, size_t len); +void mumble_set_identity(const char* identity); + +void mumble_unlink(void); + +#endif // _LIBMUMBLELINK_H_ diff --git a/src/client/qal.cpp b/src/client/qal.cpp new file mode 100644 index 0000000..466a45e --- /dev/null +++ b/src/client/qal.cpp @@ -0,0 +1,337 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2005 Stuart Dalton (badcdev@gmail.com) +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +// Dynamically loads OpenAL + +#ifdef USE_OPENAL + +#include "qal.h" + +#ifdef USE_OPENAL_DLOPEN +#include "sys/sys_loadlib.h" + +LPALENABLE qalEnable; +LPALDISABLE qalDisable; +LPALISENABLED qalIsEnabled; +LPALGETSTRING qalGetString; +LPALGETBOOLEANV qalGetBooleanv; +LPALGETINTEGERV qalGetIntegerv; +LPALGETFLOATV qalGetFloatv; +LPALGETDOUBLEV qalGetDoublev; +LPALGETBOOLEAN qalGetBoolean; +LPALGETINTEGER qalGetInteger; +LPALGETFLOAT qalGetFloat; +LPALGETDOUBLE qalGetDouble; +LPALGETERROR qalGetError; +LPALISEXTENSIONPRESENT qalIsExtensionPresent; +LPALGETPROCADDRESS qalGetProcAddress; +LPALGETENUMVALUE qalGetEnumValue; +LPALLISTENERF qalListenerf; +LPALLISTENER3F qalListener3f; +LPALLISTENERFV qalListenerfv; +LPALLISTENERI qalListeneri; +LPALGETLISTENERF qalGetListenerf; +LPALGETLISTENER3F qalGetListener3f; +LPALGETLISTENERFV qalGetListenerfv; +LPALGETLISTENERI qalGetListeneri; +LPALGENSOURCES qalGenSources; +LPALDELETESOURCES qalDeleteSources; +LPALISSOURCE qalIsSource; +LPALSOURCEF qalSourcef; +LPALSOURCE3F qalSource3f; +LPALSOURCEFV qalSourcefv; +LPALSOURCEI qalSourcei; +LPALGETSOURCEF qalGetSourcef; +LPALGETSOURCE3F qalGetSource3f; +LPALGETSOURCEFV qalGetSourcefv; +LPALGETSOURCEI qalGetSourcei; +LPALSOURCEPLAYV qalSourcePlayv; +LPALSOURCESTOPV qalSourceStopv; +LPALSOURCEREWINDV qalSourceRewindv; +LPALSOURCEPAUSEV qalSourcePausev; +LPALSOURCEPLAY qalSourcePlay; +LPALSOURCESTOP qalSourceStop; +LPALSOURCEREWIND qalSourceRewind; +LPALSOURCEPAUSE qalSourcePause; +LPALSOURCEQUEUEBUFFERS qalSourceQueueBuffers; +LPALSOURCEUNQUEUEBUFFERS qalSourceUnqueueBuffers; +LPALGENBUFFERS qalGenBuffers; +LPALDELETEBUFFERS qalDeleteBuffers; +LPALISBUFFER qalIsBuffer; +LPALBUFFERDATA qalBufferData; +LPALGETBUFFERF qalGetBufferf; +LPALGETBUFFERI qalGetBufferi; +LPALDOPPLERFACTOR qalDopplerFactor; +LPALSPEEDOFSOUND qalSpeedOfSound; +LPALDISTANCEMODEL qalDistanceModel; + +LPALCCREATECONTEXT qalcCreateContext; +LPALCMAKECONTEXTCURRENT qalcMakeContextCurrent; +LPALCPROCESSCONTEXT qalcProcessContext; +LPALCSUSPENDCONTEXT qalcSuspendContext; +LPALCDESTROYCONTEXT qalcDestroyContext; +LPALCGETCURRENTCONTEXT qalcGetCurrentContext; +LPALCGETCONTEXTSDEVICE qalcGetContextsDevice; +LPALCOPENDEVICE qalcOpenDevice; +LPALCCLOSEDEVICE qalcCloseDevice; +LPALCGETERROR qalcGetError; +LPALCISEXTENSIONPRESENT qalcIsExtensionPresent; +LPALCGETPROCADDRESS qalcGetProcAddress; +LPALCGETENUMVALUE qalcGetEnumValue; +LPALCGETSTRING qalcGetString; +LPALCGETINTEGERV qalcGetIntegerv; +LPALCCAPTUREOPENDEVICE qalcCaptureOpenDevice; +LPALCCAPTURECLOSEDEVICE qalcCaptureCloseDevice; +LPALCCAPTURESTART qalcCaptureStart; +LPALCCAPTURESTOP qalcCaptureStop; +LPALCCAPTURESAMPLES qalcCaptureSamples; + +static void *OpenALLib = NULL; + +static bool alinit_fail = false; + +/* +================= +GPA +================= +*/ +static void *GPA(const char *str) +{ + void *rv; + + rv = Sys_LoadFunction(OpenALLib, str); + if(!rv) + { + Com_Printf( " Can't load symbol %s\n", str); + alinit_fail = true; + return NULL; + } + else + { + Com_DPrintf( " Loaded symbol %s (%p)\n", str, rv); + return rv; + } +} + +/* +================= +QAL_Init +================= +*/ +bool QAL_Init(const char *libname) +{ + if(OpenALLib) + return true; + + if(!(OpenALLib = Sys_LoadDll(libname, true))) + return false; + + alinit_fail = false; + + qalEnable = (LPALENABLE)GPA("alEnable"); + qalDisable = (LPALDISABLE)GPA("alDisable"); + qalIsEnabled = (LPALISENABLED)GPA("alIsEnabled"); + qalGetString = (LPALGETSTRING)GPA("alGetString"); + qalGetBooleanv = (LPALGETBOOLEANV)GPA("alGetBooleanv"); + qalGetIntegerv = (LPALGETINTEGERV)GPA("alGetIntegerv"); + qalGetFloatv = (LPALGETFLOATV)GPA("alGetFloatv"); + qalGetDoublev = (LPALGETDOUBLEV)GPA("alGetDoublev"); + qalGetBoolean = (LPALGETBOOLEAN)GPA("alGetBoolean"); + qalGetInteger = (LPALGETINTEGER)GPA("alGetInteger"); + qalGetFloat = (LPALGETFLOAT)GPA("alGetFloat"); + qalGetDouble = (LPALGETDOUBLE)GPA("alGetDouble"); + qalGetError = (LPALGETERROR)GPA("alGetError"); + qalIsExtensionPresent = (LPALISEXTENSIONPRESENT)GPA("alIsExtensionPresent"); + qalGetProcAddress = (LPALGETPROCADDRESS)GPA("alGetProcAddress"); + qalGetEnumValue = (LPALGETENUMVALUE)GPA("alGetEnumValue"); + qalListenerf = (LPALLISTENERF)GPA("alListenerf"); + qalListener3f = (LPALLISTENER3F)GPA("alListener3f"); + qalListenerfv = (LPALLISTENERFV)GPA("alListenerfv"); + qalListeneri = (LPALLISTENERI)GPA("alListeneri"); + qalGetListenerf = (LPALGETLISTENERF)GPA("alGetListenerf"); + qalGetListener3f = (LPALGETLISTENER3F)GPA("alGetListener3f"); + qalGetListenerfv = (LPALGETLISTENERFV)GPA("alGetListenerfv"); + qalGetListeneri = (LPALGETLISTENERI)GPA("alGetListeneri"); + qalGenSources = (LPALGENSOURCES)GPA("alGenSources"); + qalDeleteSources = (LPALDELETESOURCES)GPA("alDeleteSources"); + qalIsSource = (LPALISSOURCE)GPA("alIsSource"); + qalSourcef = (LPALSOURCEF)GPA("alSourcef"); + qalSource3f = (LPALSOURCE3F)GPA("alSource3f"); + qalSourcefv = (LPALSOURCEFV)GPA("alSourcefv"); + qalSourcei = (LPALSOURCEI)GPA("alSourcei"); + qalGetSourcef = (LPALGETSOURCEF)GPA("alGetSourcef"); + qalGetSource3f = (LPALGETSOURCE3F)GPA("alGetSource3f"); + qalGetSourcefv = (LPALGETSOURCEFV)GPA("alGetSourcefv"); + qalGetSourcei = (LPALGETSOURCEI)GPA("alGetSourcei"); + qalSourcePlayv = (LPALSOURCEPLAYV)GPA("alSourcePlayv"); + qalSourceStopv = (LPALSOURCESTOPV)GPA("alSourceStopv"); + qalSourceRewindv = (LPALSOURCEREWINDV)GPA("alSourceRewindv"); + qalSourcePausev = (LPALSOURCEPAUSEV)GPA("alSourcePausev"); + qalSourcePlay = (LPALSOURCEPLAY)GPA("alSourcePlay"); + qalSourceStop = (LPALSOURCESTOP)GPA("alSourceStop"); + qalSourceRewind = (LPALSOURCEREWIND)GPA("alSourceRewind"); + qalSourcePause = (LPALSOURCEPAUSE)GPA("alSourcePause"); + qalSourceQueueBuffers = (LPALSOURCEQUEUEBUFFERS)GPA("alSourceQueueBuffers"); + qalSourceUnqueueBuffers = (LPALSOURCEUNQUEUEBUFFERS)GPA("alSourceUnqueueBuffers"); + qalGenBuffers = (LPALGENBUFFERS)GPA("alGenBuffers"); + qalDeleteBuffers = (LPALDELETEBUFFERS)GPA("alDeleteBuffers"); + qalIsBuffer = (LPALISBUFFER)GPA("alIsBuffer"); + qalBufferData = (LPALBUFFERDATA)GPA("alBufferData"); + qalGetBufferf = (LPALGETBUFFERF)GPA("alGetBufferf"); + qalGetBufferi = (LPALGETBUFFERI)GPA("alGetBufferi"); + qalDopplerFactor = (LPALDOPPLERFACTOR)GPA("alDopplerFactor"); + qalSpeedOfSound = (LPALSPEEDOFSOUND)GPA("alSpeedOfSound"); + qalDistanceModel = (LPALDISTANCEMODEL)GPA("alDistanceModel"); + + qalcCreateContext = (LPALCCREATECONTEXT)GPA("alcCreateContext"); + qalcMakeContextCurrent = (LPALCMAKECONTEXTCURRENT)GPA("alcMakeContextCurrent"); + qalcProcessContext = (LPALCPROCESSCONTEXT)GPA("alcProcessContext"); + qalcSuspendContext = (LPALCSUSPENDCONTEXT)GPA("alcSuspendContext"); + qalcDestroyContext = (LPALCDESTROYCONTEXT)GPA("alcDestroyContext"); + qalcGetCurrentContext = (LPALCGETCURRENTCONTEXT)GPA("alcGetCurrentContext"); + qalcGetContextsDevice = (LPALCGETCONTEXTSDEVICE)GPA("alcGetContextsDevice"); + qalcOpenDevice = (LPALCOPENDEVICE)GPA("alcOpenDevice"); + qalcCloseDevice = (LPALCCLOSEDEVICE)GPA("alcCloseDevice"); + qalcGetError = (LPALCGETERROR)GPA("alcGetError"); + qalcIsExtensionPresent = (LPALCISEXTENSIONPRESENT)GPA("alcIsExtensionPresent"); + qalcGetProcAddress = (LPALCGETPROCADDRESS)GPA("alcGetProcAddress"); + qalcGetEnumValue = (LPALCGETENUMVALUE)GPA("alcGetEnumValue"); + qalcGetString = (LPALCGETSTRING)GPA("alcGetString"); + qalcGetIntegerv = (LPALCGETINTEGERV)GPA("alcGetIntegerv"); + qalcCaptureOpenDevice = (LPALCCAPTUREOPENDEVICE)GPA("alcCaptureOpenDevice"); + qalcCaptureCloseDevice = (LPALCCAPTURECLOSEDEVICE)GPA("alcCaptureCloseDevice"); + qalcCaptureStart = (LPALCCAPTURESTART)GPA("alcCaptureStart"); + qalcCaptureStop = (LPALCCAPTURESTOP)GPA("alcCaptureStop"); + qalcCaptureSamples = (LPALCCAPTURESAMPLES)GPA("alcCaptureSamples"); + + if(alinit_fail) + { + QAL_Shutdown(); + Com_Printf( " One or more symbols not found\n"); + return false; + } + + return true; +} + +/* +================= +QAL_Shutdown +================= +*/ +void QAL_Shutdown( void ) +{ + if(OpenALLib) + { + Sys_UnloadLibrary(OpenALLib); + OpenALLib = NULL; + } + + qalEnable = NULL; + qalDisable = NULL; + qalIsEnabled = NULL; + qalGetString = NULL; + qalGetBooleanv = NULL; + qalGetIntegerv = NULL; + qalGetFloatv = NULL; + qalGetDoublev = NULL; + qalGetBoolean = NULL; + qalGetInteger = NULL; + qalGetFloat = NULL; + qalGetDouble = NULL; + qalGetError = NULL; + qalIsExtensionPresent = NULL; + qalGetProcAddress = NULL; + qalGetEnumValue = NULL; + qalListenerf = NULL; + qalListener3f = NULL; + qalListenerfv = NULL; + qalListeneri = NULL; + qalGetListenerf = NULL; + qalGetListener3f = NULL; + qalGetListenerfv = NULL; + qalGetListeneri = NULL; + qalGenSources = NULL; + qalDeleteSources = NULL; + qalIsSource = NULL; + qalSourcef = NULL; + qalSource3f = NULL; + qalSourcefv = NULL; + qalSourcei = NULL; + qalGetSourcef = NULL; + qalGetSource3f = NULL; + qalGetSourcefv = NULL; + qalGetSourcei = NULL; + qalSourcePlayv = NULL; + qalSourceStopv = NULL; + qalSourceRewindv = NULL; + qalSourcePausev = NULL; + qalSourcePlay = NULL; + qalSourceStop = NULL; + qalSourceRewind = NULL; + qalSourcePause = NULL; + qalSourceQueueBuffers = NULL; + qalSourceUnqueueBuffers = NULL; + qalGenBuffers = NULL; + qalDeleteBuffers = NULL; + qalIsBuffer = NULL; + qalBufferData = NULL; + qalGetBufferf = NULL; + qalGetBufferi = NULL; + qalDopplerFactor = NULL; + qalSpeedOfSound = NULL; + qalDistanceModel = NULL; + + qalcCreateContext = NULL; + qalcMakeContextCurrent = NULL; + qalcProcessContext = NULL; + qalcSuspendContext = NULL; + qalcDestroyContext = NULL; + qalcGetCurrentContext = NULL; + qalcGetContextsDevice = NULL; + qalcOpenDevice = NULL; + qalcCloseDevice = NULL; + qalcGetError = NULL; + qalcIsExtensionPresent = NULL; + qalcGetProcAddress = NULL; + qalcGetEnumValue = NULL; + qalcGetString = NULL; + qalcGetIntegerv = NULL; + qalcCaptureOpenDevice = NULL; + qalcCaptureCloseDevice = NULL; + qalcCaptureStart = NULL; + qalcCaptureStop = NULL; + qalcCaptureSamples = NULL; +} +#else +bool QAL_Init(const char *libname) +{ + return true; +} +void QAL_Shutdown( void ) +{ +} +#endif +#endif diff --git a/src/client/qal.h b/src/client/qal.h new file mode 100644 index 0000000..41d6244 --- /dev/null +++ b/src/client/qal.h @@ -0,0 +1,252 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2005 Stuart Dalton (badcdev@gmail.com) +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#ifndef __QAL_H__ +#define __QAL_H__ + +#ifdef USE_LOCAL_HEADERS +# include "AL/al.h" +# include "AL/alc.h" +#else +# if defined(_MSC_VER) || defined(__APPLE__) +// MSVC users must install the OpenAL SDK which doesn't use the AL/*.h scheme. +// OSX framework also needs this +# include <al.h> +# include <alc.h> +# else +# include <AL/al.h> +# include <AL/alc.h> +# endif +#endif + +#include "qcommon/q_shared.h" +#include "qcommon/qcommon.h" + +#ifdef USE_OPENAL_DLOPEN +#define AL_NO_PROTOTYPES +#define ALC_NO_PROTOTYPES +#endif + +/* Hack to enable compiling both on OpenAL SDK and OpenAL-soft. */ +#ifndef ALC_ENUMERATE_ALL_EXT +# define ALC_ENUMERATE_ALL_EXT 1 +# define ALC_DEFAULT_ALL_DEVICES_SPECIFIER 0x1012 +# define ALC_ALL_DEVICES_SPECIFIER 0x1013 +#endif + +#ifdef USE_OPENAL_DLOPEN +extern LPALENABLE qalEnable; +extern LPALDISABLE qalDisable; +extern LPALISENABLED qalIsEnabled; +extern LPALGETSTRING qalGetString; +extern LPALGETBOOLEANV qalGetBooleanv; +extern LPALGETINTEGERV qalGetIntegerv; +extern LPALGETFLOATV qalGetFloatv; +extern LPALGETDOUBLEV qalGetDoublev; +extern LPALGETBOOLEAN qalGetBoolean; +extern LPALGETINTEGER qalGetInteger; +extern LPALGETFLOAT qalGetFloat; +extern LPALGETDOUBLE qalGetDouble; +extern LPALGETERROR qalGetError; +extern LPALISEXTENSIONPRESENT qalIsExtensionPresent; +extern LPALGETPROCADDRESS qalGetProcAddress; +extern LPALGETENUMVALUE qalGetEnumValue; +extern LPALLISTENERF qalListenerf; +extern LPALLISTENER3F qalListener3f; +extern LPALLISTENERFV qalListenerfv; +extern LPALLISTENERI qalListeneri; +extern LPALLISTENER3I qalListener3i; +extern LPALLISTENERIV qalListeneriv; +extern LPALGETLISTENERF qalGetListenerf; +extern LPALGETLISTENER3F qalGetListener3f; +extern LPALGETLISTENERFV qalGetListenerfv; +extern LPALGETLISTENERI qalGetListeneri; +extern LPALGETLISTENER3I qalGetListener3i; +extern LPALGETLISTENERIV qalGetListeneriv; +extern LPALGENSOURCES qalGenSources; +extern LPALDELETESOURCES qalDeleteSources; +extern LPALISSOURCE qalIsSource; +extern LPALSOURCEF qalSourcef; +extern LPALSOURCE3F qalSource3f; +extern LPALSOURCEFV qalSourcefv; +extern LPALSOURCEI qalSourcei; +extern LPALSOURCE3I qalSource3i; +extern LPALSOURCEIV qalSourceiv; +extern LPALGETSOURCEF qalGetSourcef; +extern LPALGETSOURCE3F qalGetSource3f; +extern LPALGETSOURCEFV qalGetSourcefv; +extern LPALGETSOURCEI qalGetSourcei; +extern LPALGETSOURCE3I qalGetSource3i; +extern LPALGETSOURCEIV qalGetSourceiv; +extern LPALSOURCEPLAYV qalSourcePlayv; +extern LPALSOURCESTOPV qalSourceStopv; +extern LPALSOURCEREWINDV qalSourceRewindv; +extern LPALSOURCEPAUSEV qalSourcePausev; +extern LPALSOURCEPLAY qalSourcePlay; +extern LPALSOURCESTOP qalSourceStop; +extern LPALSOURCEREWIND qalSourceRewind; +extern LPALSOURCEPAUSE qalSourcePause; +extern LPALSOURCEQUEUEBUFFERS qalSourceQueueBuffers; +extern LPALSOURCEUNQUEUEBUFFERS qalSourceUnqueueBuffers; +extern LPALGENBUFFERS qalGenBuffers; +extern LPALDELETEBUFFERS qalDeleteBuffers; +extern LPALISBUFFER qalIsBuffer; +extern LPALBUFFERDATA qalBufferData; +extern LPALBUFFERF qalBufferf; +extern LPALBUFFER3F qalBuffer3f; +extern LPALBUFFERFV qalBufferfv; +extern LPALBUFFERF qalBufferi; +extern LPALBUFFER3F qalBuffer3i; +extern LPALBUFFERFV qalBufferiv; +extern LPALGETBUFFERF qalGetBufferf; +extern LPALGETBUFFER3F qalGetBuffer3f; +extern LPALGETBUFFERFV qalGetBufferfv; +extern LPALGETBUFFERI qalGetBufferi; +extern LPALGETBUFFER3I qalGetBuffer3i; +extern LPALGETBUFFERIV qalGetBufferiv; +extern LPALDOPPLERFACTOR qalDopplerFactor; +extern LPALSPEEDOFSOUND qalSpeedOfSound; +extern LPALDISTANCEMODEL qalDistanceModel; + +extern LPALCCREATECONTEXT qalcCreateContext; +extern LPALCMAKECONTEXTCURRENT qalcMakeContextCurrent; +extern LPALCPROCESSCONTEXT qalcProcessContext; +extern LPALCSUSPENDCONTEXT qalcSuspendContext; +extern LPALCDESTROYCONTEXT qalcDestroyContext; +extern LPALCGETCURRENTCONTEXT qalcGetCurrentContext; +extern LPALCGETCONTEXTSDEVICE qalcGetContextsDevice; +extern LPALCOPENDEVICE qalcOpenDevice; +extern LPALCCLOSEDEVICE qalcCloseDevice; +extern LPALCGETERROR qalcGetError; +extern LPALCISEXTENSIONPRESENT qalcIsExtensionPresent; +extern LPALCGETPROCADDRESS qalcGetProcAddress; +extern LPALCGETENUMVALUE qalcGetEnumValue; +extern LPALCGETSTRING qalcGetString; +extern LPALCGETINTEGERV qalcGetIntegerv; +extern LPALCCAPTUREOPENDEVICE qalcCaptureOpenDevice; +extern LPALCCAPTURECLOSEDEVICE qalcCaptureCloseDevice; +extern LPALCCAPTURESTART qalcCaptureStart; +extern LPALCCAPTURESTOP qalcCaptureStop; +extern LPALCCAPTURESAMPLES qalcCaptureSamples; +#else +#define qalEnable alEnable +#define qalDisable alDisable +#define qalIsEnabled alIsEnabled +#define qalGetString alGetString +#define qalGetBooleanv alGetBooleanv +#define qalGetIntegerv alGetIntegerv +#define qalGetFloatv alGetFloatv +#define qalGetDoublev alGetDoublev +#define qalGetBoolean alGetBoolean +#define qalGetInteger alGetInteger +#define qalGetFloat alGetFloat +#define qalGetDouble alGetDouble +#define qalGetError alGetError +#define qalIsExtensionPresent alIsExtensionPresent +#define qalGetProcAddress alGetProcAddress +#define qalGetEnumValue alGetEnumValue +#define qalListenerf alListenerf +#define qalListener3f alListener3f +#define qalListenerfv alListenerfv +#define qalListeneri alListeneri +#define qalListener3i alListener3i +#define qalListeneriv alListeneriv +#define qalGetListenerf alGetListenerf +#define qalGetListener3f alGetListener3f +#define qalGetListenerfv alGetListenerfv +#define qalGetListeneri alGetListeneri +#define qalGetListener3i alGetListener3i +#define qalGetListeneriv alGetListeneriv +#define qalGenSources alGenSources +#define qalDeleteSources alDeleteSources +#define qalIsSource alIsSource +#define qalSourcef alSourcef +#define qalSource3f alSource3f +#define qalSourcefv alSourcefv +#define qalSourcei alSourcei +#define qalSource3i alSource3i +#define qalSourceiv alSourceiv +#define qalGetSourcef alGetSourcef +#define qalGetSource3f alGetSource3f +#define qalGetSourcefv alGetSourcefv +#define qalGetSourcei alGetSourcei +#define qalGetSource3i alGetSource3i +#define qalGetSourceiv alGetSourceiv +#define qalSourcePlayv alSourcePlayv +#define qalSourceStopv alSourceStopv +#define qalSourceRewindv alSourceRewindv +#define qalSourcePausev alSourcePausev +#define qalSourcePlay alSourcePlay +#define qalSourceStop alSourceStop +#define qalSourceRewind alSourceRewind +#define qalSourcePause alSourcePause +#define qalSourceQueueBuffers alSourceQueueBuffers +#define qalSourceUnqueueBuffers alSourceUnqueueBuffers +#define qalGenBuffers alGenBuffers +#define qalDeleteBuffers alDeleteBuffers +#define qalIsBuffer alIsBuffer +#define qalBufferData alBufferData +#define qalBufferf alBufferf +#define qalBuffer3f alBuffer3f +#define qalBufferfv alBufferfv +#define qalBufferi alBufferi +#define qalBuffer3i alBuffer3i +#define qalBufferiv alBufferiv +#define qalGetBufferf alGetBufferf +#define qalGetBuffer3f alGetBuffer3f +#define qalGetBufferfv alGetBufferfv +#define qalGetBufferi alGetBufferi +#define qalGetBuffer3i alGetBuffer3i +#define qalGetBufferiv alGetBufferiv +#define qalDopplerFactor alDopplerFactor +#define qalSpeedOfSound alSpeedOfSound +#define qalDistanceModel alDistanceModel + +#define qalcCreateContext alcCreateContext +#define qalcMakeContextCurrent alcMakeContextCurrent +#define qalcProcessContext alcProcessContext +#define qalcSuspendContext alcSuspendContext +#define qalcDestroyContext alcDestroyContext +#define qalcGetCurrentContext alcGetCurrentContext +#define qalcGetContextsDevice alcGetContextsDevice +#define qalcOpenDevice alcOpenDevice +#define qalcCloseDevice alcCloseDevice +#define qalcGetError alcGetError +#define qalcIsExtensionPresent alcIsExtensionPresent +#define qalcGetProcAddress alcGetProcAddress +#define qalcGetEnumValue alcGetEnumValue +#define qalcGetString alcGetString +#define qalcGetIntegerv alcGetIntegerv +#define qalcCaptureOpenDevice alcCaptureOpenDevice +#define qalcCaptureCloseDevice alcCaptureCloseDevice +#define qalcCaptureStart alcCaptureStart +#define qalcCaptureStop alcCaptureStop +#define qalcCaptureSamples alcCaptureSamples +#endif + +bool QAL_Init(const char *libname); +void QAL_Shutdown( void ); + +#endif // __QAL_H__ diff --git a/src/client/snd_adpcm.cpp b/src/client/snd_adpcm.cpp new file mode 100644 index 0000000..d5d9930 --- /dev/null +++ b/src/client/snd_adpcm.cpp @@ -0,0 +1,329 @@ +/*********************************************************** +Copyright 1992 by Stichting Mathematisch Centrum, Amsterdam, The +Netherlands. + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the names of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +******************************************************************/ + +/* +** Intel/DVI ADPCM coder/decoder. +** +** The algorithm for this coder was taken from the IMA Compatability Project +** proceedings, Vol 2, Number 2; May 1992. +** +** Version 1.2, 18-Dec-92. +*/ + +#include "snd_local.h" + +/* Intel ADPCM step variation table */ +static int indexTable[16] = { + -1, -1, -1, -1, 2, 4, 6, 8, + -1, -1, -1, -1, 2, 4, 6, 8, +}; + +static int stepsizeTable[89] = { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, + 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, + 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, + 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, + 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, + 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, + 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, + 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767 +}; + + +void S_AdpcmEncode( short indata[], char outdata[], int len, struct adpcm_state *state ) { + short *inp; /* Input buffer pointer */ + signed char *outp; /* output buffer pointer */ + int val; /* Current input sample value */ + int sign; /* Current adpcm sign bit */ + int delta; /* Current adpcm output value */ + int diff; /* Difference between val and sample */ + int step; /* Stepsize */ + int valpred; /* Predicted output value */ + int vpdiff; /* Current change to valpred */ + int index; /* Current step change index */ + int outputbuffer; /* place to keep previous 4-bit value */ + int bufferstep; /* toggle between outputbuffer/output */ + + outp = (signed char *)outdata; + inp = indata; + + valpred = state->sample; + index = state->index; + step = stepsizeTable[index]; + + outputbuffer = 0; // quiet a compiler warning + bufferstep = 1; + + for ( ; len > 0 ; len-- ) { + val = *inp++; + + /* Step 1 - compute difference with previous value */ + diff = val - valpred; + sign = (diff < 0) ? 8 : 0; + if ( sign ) diff = (-diff); + + /* Step 2 - Divide and clamp */ + /* Note: + ** This code *approximately* computes: + ** delta = diff*4/step; + ** vpdiff = (delta+0.5)*step/4; + ** but in shift step bits are dropped. The net result of this is + ** that even if you have fast mul/div hardware you cannot put it to + ** good use since the fixup would be too expensive. + */ + delta = 0; + vpdiff = (step >> 3); + + if ( diff >= step ) { + delta = 4; + diff -= step; + vpdiff += step; + } + step >>= 1; + if ( diff >= step ) { + delta |= 2; + diff -= step; + vpdiff += step; + } + step >>= 1; + if ( diff >= step ) { + delta |= 1; + vpdiff += step; + } + + /* Step 3 - Update previous value */ + if ( sign ) + valpred -= vpdiff; + else + valpred += vpdiff; + + /* Step 4 - Clamp previous value to 16 bits */ + if ( valpred > 32767 ) + valpred = 32767; + else if ( valpred < -32768 ) + valpred = -32768; + + /* Step 5 - Assemble value, update index and step values */ + delta |= sign; + + index += indexTable[delta]; + if ( index < 0 ) index = 0; + if ( index > 88 ) index = 88; + step = stepsizeTable[index]; + + /* Step 6 - Output value */ + if ( bufferstep ) { + outputbuffer = (delta << 4) & 0xf0; + } else { + *outp++ = (delta & 0x0f) | outputbuffer; + } + bufferstep = !bufferstep; + } + + /* Output last step, if needed */ + if ( !bufferstep ) + *outp++ = outputbuffer; + + state->sample = valpred; + state->index = index; +} + + +/* static */ void S_AdpcmDecode( const char indata[], short *outdata, int len, struct adpcm_state *state ) { + signed char *inp; /* Input buffer pointer */ + int outp; /* output buffer pointer */ + int sign; /* Current adpcm sign bit */ + int delta; /* Current adpcm output value */ + int step; /* Stepsize */ + int valpred; /* Predicted value */ + int vpdiff; /* Current change to valpred */ + int index; /* Current step change index */ + int inputbuffer; /* place to keep next 4-bit value */ + int bufferstep; /* toggle between inputbuffer/input */ + + outp = 0; + inp = (signed char *)indata; + + valpred = state->sample; + index = state->index; + step = stepsizeTable[index]; + + bufferstep = 0; + inputbuffer = 0; // quiet a compiler warning + for ( ; len > 0 ; len-- ) { + + /* Step 1 - get the delta value */ + if ( bufferstep ) { + delta = inputbuffer & 0xf; + } else { + inputbuffer = *inp++; + delta = (inputbuffer >> 4) & 0xf; + } + bufferstep = !bufferstep; + + /* Step 2 - Find new index value (for later) */ + index += indexTable[delta]; + if ( index < 0 ) index = 0; + if ( index > 88 ) index = 88; + + /* Step 3 - Separate sign and magnitude */ + sign = delta & 8; + delta = delta & 7; + + /* Step 4 - Compute difference and new predicted value */ + /* + ** Computes 'vpdiff = (delta+0.5)*step/4', but see comment + ** in adpcm_coder. + */ + vpdiff = step >> 3; + if ( delta & 4 ) vpdiff += step; + if ( delta & 2 ) vpdiff += step>>1; + if ( delta & 1 ) vpdiff += step>>2; + + if ( sign ) + valpred -= vpdiff; + else + valpred += vpdiff; + + /* Step 5 - clamp output value */ + if ( valpred > 32767 ) + valpred = 32767; + else if ( valpred < -32768 ) + valpred = -32768; + + /* Step 6 - Update step value */ + step = stepsizeTable[index]; + + /* Step 7 - Output value */ + outdata[outp] = valpred; + outp++; + } + + state->sample = valpred; + state->index = index; +} + + +/* +==================== +S_AdpcmMemoryNeeded + +Returns the amount of memory (in bytes) needed to store the samples in out internal adpcm format +==================== +*/ +int S_AdpcmMemoryNeeded( const wavinfo_t *info ) { + float scale; + int scaledSampleCount; + int sampleMemory; + int blockCount; + int headerMemory; + + // determine scale to convert from input sampling rate to desired sampling rate + scale = (float)info->rate / dma.speed; + + // calc number of samples at playback sampling rate + scaledSampleCount = info->samples / scale; + + // calc memory need to store those samples using ADPCM at 4 bits per sample + sampleMemory = scaledSampleCount / 2; + + // calc number of sample blocks needed of PAINTBUFFER_SIZE + blockCount = scaledSampleCount / PAINTBUFFER_SIZE; + if( scaledSampleCount % PAINTBUFFER_SIZE ) { + blockCount++; + } + + // calc memory needed to store the block headers + headerMemory = blockCount * sizeof(adpcm_state_t); + + return sampleMemory + headerMemory; +} + + +/* +==================== +S_AdpcmGetSamples +==================== +*/ +void S_AdpcmGetSamples(sndBuffer *chunk, short *to) { + adpcm_state_t state; + byte *out; + + // get the starting state from the block header + state.index = chunk->adpcm.index; + state.sample = chunk->adpcm.sample; + + out = (byte *)chunk->sndChunk; + // get samples + S_AdpcmDecode((char *) out, to, SND_CHUNK_SIZE_BYTE*2, &state ); +} + + +/* +==================== +S_AdpcmEncodeSound +==================== +*/ +void S_AdpcmEncodeSound( sfx_t *sfx, short *samples ) { + adpcm_state_t state; + int inOffset; + int count; + int n; + sndBuffer *newchunk, *chunk; + byte *out; + + inOffset = 0; + count = sfx->soundLength; + state.index = 0; + state.sample = samples[0]; + + chunk = NULL; + while( count ) { + n = count; + if( n > SND_CHUNK_SIZE_BYTE*2 ) { + n = SND_CHUNK_SIZE_BYTE*2; + } + + newchunk = SND_malloc(); + if (sfx->soundData == NULL) { + sfx->soundData = newchunk; + } else if (chunk != NULL) { + chunk->next = newchunk; + } + chunk = newchunk; + + // output the header + chunk->adpcm.index = state.index; + chunk->adpcm.sample = state.sample; + + out = (byte *)chunk->sndChunk; + + // encode the samples + S_AdpcmEncode( samples + inOffset, (char *) out, n, &state ); + + inOffset += n; + count -= n; + } +} diff --git a/src/client/snd_codec.cpp b/src/client/snd_codec.cpp new file mode 100644 index 0000000..a794b90 --- /dev/null +++ b/src/client/snd_codec.cpp @@ -0,0 +1,239 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2005 Stuart Dalton (badcdev@gmail.com) +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#include "client.h" +#include "snd_codec.h" + +static snd_codec_t *codecs; + +/* +================= +S_CodecGetSound + +Opens/loads a sound, tries codec based on the sound's file extension +then tries all supported codecs. +================= +*/ +static void *S_CodecGetSound(const char *filename, snd_info_t *info) +{ + snd_codec_t *codec; + snd_codec_t *orgCodec = NULL; + bool orgNameFailed = false; + char localName[ MAX_QPATH ]; + const char *ext; + char altName[ MAX_QPATH ]; + void *rtn = NULL; + + Q_strncpyz(localName, filename, MAX_QPATH); + + ext = COM_GetExtension(localName); + + if( *ext ) + { + // Look for the correct loader and use it + for( codec = codecs; codec; codec = codec->next ) + { + if( !Q_stricmp( ext, codec->ext ) ) + { + // Load + if( info ) + rtn = codec->load(localName, info); + else + rtn = codec->open(localName); + break; + } + } + + // A loader was found + if( codec ) + { + if( !rtn ) + { + // Loader failed, most likely because the file isn't there; + // try again without the extension + orgNameFailed = true; + orgCodec = codec; + COM_StripExtension( filename, localName, MAX_QPATH ); + } + else + { + // Something loaded + return rtn; + } + } + } + + // Try and find a suitable match using all + // the sound codecs supported + for( codec = codecs; codec; codec = codec->next ) + { + if( codec == orgCodec ) + continue; + + Com_sprintf( altName, sizeof (altName), "%s.%s", localName, codec->ext ); + + // Load + if( info ) + rtn = codec->load(altName, info); + else + rtn = codec->open(altName); + + if( rtn ) + { + if( orgNameFailed ) + { + Com_DPrintf(S_COLOR_YELLOW "WARNING: %s not present, using %s instead\n", + filename, altName ); + } + + return rtn; + } + } + + Com_Printf(S_COLOR_YELLOW "WARNING: Failed to %s sound %s!\n", info ? "load" : "open", filename); + + return NULL; +} + +/* +================= +S_CodecInit +================= +*/ +void S_CodecInit() +{ + codecs = NULL; + +#ifdef USE_CODEC_OPUS + S_CodecRegister(&opus_codec); +#endif + +#ifdef USE_CODEC_VORBIS + S_CodecRegister(&ogg_codec); +#endif + +// Register wav codec last so that it is always tried first when a file extension was not found + S_CodecRegister(&wav_codec); +} + +/* +================= +S_CodecShutdown +================= +*/ +void S_CodecShutdown() +{ + codecs = NULL; +} + +/* +================= +S_CodecRegister +================= +*/ +void S_CodecRegister(snd_codec_t *codec) +{ + codec->next = codecs; + codecs = codec; +} + +/* +================= +S_CodecLoad +================= +*/ +void *S_CodecLoad(const char *filename, snd_info_t *info) +{ + return S_CodecGetSound(filename, info); +} + +/* +================= +S_CodecOpenStream +================= +*/ +snd_stream_t *S_CodecOpenStream(const char *filename) +{ + return (snd_stream_t*)S_CodecGetSound(filename, NULL); +} + +void S_CodecCloseStream(snd_stream_t *stream) +{ + stream->codec->close(stream); +} + +int S_CodecReadStream(snd_stream_t *stream, int bytes, void *buffer) +{ + return stream->codec->read(stream, bytes, buffer); +} + +//======================================================================= +// Util functions (used by codecs) + +/* +================= +S_CodecUtilOpen +================= +*/ +snd_stream_t *S_CodecUtilOpen(const char *filename, snd_codec_t *codec) +{ + snd_stream_t *stream; + fileHandle_t hnd; + int length; + + // Try to open the file + length = FS_FOpenFileRead(filename, &hnd, true); + if(!hnd) + { + Com_DPrintf("Can't read sound file %s\n", filename); + return NULL; + } + + // Allocate a stream + stream = (snd_stream_t*)Z_Malloc(sizeof(snd_stream_t)); + if(!stream) + { + FS_FCloseFile(hnd); + return NULL; + } + + // Copy over, return + stream->codec = codec; + stream->file = hnd; + stream->length = length; + return stream; +} + +/* +================= +S_CodecUtilClose +================= +*/ +void S_CodecUtilClose(snd_stream_t **stream) +{ + FS_FCloseFile((*stream)->file); + Z_Free(*stream); + *stream = NULL; +} diff --git a/src/client/snd_codec.h b/src/client/snd_codec.h new file mode 100644 index 0000000..a5f4358 --- /dev/null +++ b/src/client/snd_codec.h @@ -0,0 +1,109 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2005 Stuart Dalton (badcdev@gmail.com) +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#ifndef _SND_CODEC_H_ +#define _SND_CODEC_H_ + +#include "qcommon/q_shared.h" +#include "qcommon/qcommon.h" + +typedef struct snd_info_s +{ + int rate; + int width; + int channels; + int samples; + int size; + int dataofs; +} snd_info_t; + +typedef struct snd_codec_s snd_codec_t; + +typedef struct snd_stream_s +{ + snd_codec_t *codec; + fileHandle_t file; + snd_info_t info; + int length; + int pos; + void *ptr; +} snd_stream_t; + +// Codec functions +typedef void *(*CODEC_LOAD)(const char *filename, snd_info_t *info); +typedef snd_stream_t *(*CODEC_OPEN)(const char *filename); +typedef int (*CODEC_READ)(snd_stream_t *stream, int bytes, void *buffer); +typedef void (*CODEC_CLOSE)(snd_stream_t *stream); + +// Codec data structure +struct snd_codec_s +{ + const char *ext; + CODEC_LOAD load; + CODEC_OPEN open; + CODEC_READ read; + CODEC_CLOSE close; + snd_codec_t *next; +}; + +// Codec management +void S_CodecInit( void ); +void S_CodecShutdown( void ); +void S_CodecRegister(snd_codec_t *codec); +void *S_CodecLoad(const char *filename, snd_info_t *info); +snd_stream_t *S_CodecOpenStream(const char *filename); +void S_CodecCloseStream(snd_stream_t *stream); +int S_CodecReadStream(snd_stream_t *stream, int bytes, void *buffer); + +// Util functions (used by codecs) +snd_stream_t *S_CodecUtilOpen(const char *filename, snd_codec_t *codec); +void S_CodecUtilClose(snd_stream_t **stream); + +// WAV Codec +extern snd_codec_t wav_codec; +void *S_WAV_CodecLoad(const char *filename, snd_info_t *info); +snd_stream_t *S_WAV_CodecOpenStream(const char *filename); +void S_WAV_CodecCloseStream(snd_stream_t *stream); +int S_WAV_CodecReadStream(snd_stream_t *stream, int bytes, void *buffer); + +// Ogg Vorbis codec +#ifdef USE_CODEC_VORBIS +extern snd_codec_t ogg_codec; +void *S_OGG_CodecLoad(const char *filename, snd_info_t *info); +snd_stream_t *S_OGG_CodecOpenStream(const char *filename); +void S_OGG_CodecCloseStream(snd_stream_t *stream); +int S_OGG_CodecReadStream(snd_stream_t *stream, int bytes, void *buffer); +#endif // USE_CODEC_VORBIS + +// Ogg Opus codec +#ifdef USE_CODEC_OPUS +extern snd_codec_t opus_codec; +void *S_OggOpus_CodecLoad(const char *filename, snd_info_t *info); +snd_stream_t *S_OggOpus_CodecOpenStream(const char *filename); +void S_OggOpus_CodecCloseStream(snd_stream_t *stream); +int S_OggOpus_CodecReadStream(snd_stream_t *stream, int bytes, void *buffer); +#endif // USE_CODEC_OPUS + +#endif // !_SND_CODEC_H_ diff --git a/src/client/snd_codec_ogg.cpp b/src/client/snd_codec_ogg.cpp new file mode 100644 index 0000000..15fcd37 --- /dev/null +++ b/src/client/snd_codec_ogg.cpp @@ -0,0 +1,479 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2005 Stuart Dalton (badcdev@gmail.com) +Copyright (C) 2005-2006 Joerg Dietrich <dietrich_joerg@gmx.de> +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +// OGG support is enabled by this define +#ifdef USE_CODEC_VORBIS + +// includes for the Q3 sound system +#include "client.h" + +// includes for the OGG codec +#define OV_EXCLUDE_STATIC_CALLBACKS +#include <vorbis/vorbisfile.h> + +#include <cerrno> + +#include "snd_codec.h" + +// The OGG codec can return the samples in a number of different formats, +// we use the standard signed short format. +#define OGG_SAMPLEWIDTH 2 + +// Q3 OGG codec +snd_codec_t ogg_codec = +{ + "ogg", + S_OGG_CodecLoad, + S_OGG_CodecOpenStream, + S_OGG_CodecReadStream, + S_OGG_CodecCloseStream, + NULL +}; + +// callbacks for vobisfile + +// fread() replacement +size_t S_OGG_Callback_read(void *ptr, size_t size, size_t nmemb, void *datasource) +{ + snd_stream_t *stream; + int byteSize = 0; + int bytesRead = 0; + size_t nMembRead = 0; + + // check if input is valid + if(!ptr) + { + errno = EFAULT; + return 0; + } + + if(!(size && nmemb)) + { + // It's not an error, caller just wants zero bytes! + errno = 0; + return 0; + } + + if(!datasource) + { + errno = EBADF; + return 0; + } + + // we use a snd_stream_t in the generic pointer to pass around + stream = (snd_stream_t *) datasource; + + // FS_Read does not support multi-byte elements + byteSize = nmemb * size; + + // read it with the Q3 function FS_Read() + bytesRead = FS_Read(ptr, byteSize, stream->file); + + // update the file position + stream->pos += bytesRead; + + // this function returns the number of elements read not the number of bytes + nMembRead = bytesRead / size; + + // even if the last member is only read partially + // it is counted as a whole in the return value + if(bytesRead % size) + { + nMembRead++; + } + + return nMembRead; +} + +// fseek() replacement +int S_OGG_Callback_seek(void *datasource, ogg_int64_t offset, int whence) +{ + snd_stream_t *stream; + int retVal = 0; + + // check if input is valid + if(!datasource) + { + errno = EBADF; + return -1; + } + + // snd_stream_t in the generic pointer + stream = (snd_stream_t *) datasource; + + // we must map the whence to its Q3 counterpart + switch(whence) + { + case SEEK_SET : + { + // set the file position in the actual file with the Q3 function + retVal = FS_Seek(stream->file, (long) offset, FS_SEEK_SET); + + // something has gone wrong, so we return here + if(retVal < 0) + { + return retVal; + } + + // keep track of file position + stream->pos = (int) offset; + break; + } + + case SEEK_CUR : + { + // set the file position in the actual file with the Q3 function + retVal = FS_Seek(stream->file, (long) offset, FS_SEEK_CUR); + + // something has gone wrong, so we return here + if(retVal < 0) + { + return retVal; + } + + // keep track of file position + stream->pos += (int) offset; + break; + } + + case SEEK_END : + { + // set the file position in the actual file with the Q3 function + retVal = FS_Seek(stream->file, (long) offset, FS_SEEK_END); + + // something has gone wrong, so we return here + if(retVal < 0) + { + return retVal; + } + + // keep track of file position + stream->pos = stream->length + (int) offset; + break; + } + + default : + { + // unknown whence, so we return an error + errno = EINVAL; + return -1; + } + } + + // stream->pos shouldn't be smaller than zero or bigger than the filesize + stream->pos = (stream->pos < 0) ? 0 : stream->pos; + stream->pos = (stream->pos > stream->length) ? stream->length : stream->pos; + + return 0; +} + +// fclose() replacement +int S_OGG_Callback_close(void *datasource) +{ + // we do nothing here and close all things manually in S_OGG_CodecCloseStream() + return 0; +} + +// ftell() replacement +long S_OGG_Callback_tell(void *datasource) +{ + snd_stream_t *stream; + + // check if input is valid + if(!datasource) + { + errno = EBADF; + return -1; + } + + // snd_stream_t in the generic pointer + stream = (snd_stream_t *) datasource; + + return (long) FS_FTell(stream->file); +} + +// the callback structure +const ov_callbacks S_OGG_Callbacks = +{ + &S_OGG_Callback_read, + &S_OGG_Callback_seek, + &S_OGG_Callback_close, + &S_OGG_Callback_tell +}; + +/* +================= +S_OGG_CodecOpenStream +================= +*/ +snd_stream_t *S_OGG_CodecOpenStream(const char *filename) +{ + snd_stream_t *stream; + + // OGG codec control structure + OggVorbis_File *vf; + + // some variables used to get informations about the OGG + vorbis_info *OGGInfo; + ogg_int64_t numSamples; + + // check if input is valid + if(!filename) + { + return NULL; + } + + // Open the stream + stream = S_CodecUtilOpen(filename, &ogg_codec); + if(!stream) + { + return NULL; + } + + // alloctate the OggVorbis_File + vf = (OggVorbis_File*)Z_Malloc(sizeof(OggVorbis_File)); + if(!vf) + { + S_CodecUtilClose(&stream); + + return NULL; + } + + // open the codec with our callbacks and stream as the generic pointer + if(ov_open_callbacks(stream, vf, NULL, 0, S_OGG_Callbacks) != 0) + { + Z_Free(vf); + + S_CodecUtilClose(&stream); + + return NULL; + } + + // the stream must be seekable + if(!ov_seekable(vf)) + { + ov_clear(vf); + + Z_Free(vf); + + S_CodecUtilClose(&stream); + + return NULL; + } + + // we only support OGGs with one substream + if(ov_streams(vf) != 1) + { + ov_clear(vf); + + Z_Free(vf); + + S_CodecUtilClose(&stream); + + return NULL; + } + + // get the info about channels and rate + OGGInfo = ov_info(vf, 0); + if(!OGGInfo) + { + ov_clear(vf); + + Z_Free(vf); + + S_CodecUtilClose(&stream); + + return NULL; + } + + // get the number of sample-frames in the OGG + numSamples = ov_pcm_total(vf, 0); + + // fill in the info-structure in the stream + stream->info.rate = OGGInfo->rate; + stream->info.width = OGG_SAMPLEWIDTH; + stream->info.channels = OGGInfo->channels; + stream->info.samples = numSamples; + stream->info.size = stream->info.samples * stream->info.channels * stream->info.width; + stream->info.dataofs = 0; + + // We use stream->pos for the file pointer in the compressed ogg file + stream->pos = 0; + + // We use the generic pointer in stream for the OGG codec control structure + stream->ptr = vf; + + return stream; +} + +/* +================= +S_OGG_CodecCloseStream +================= +*/ +void S_OGG_CodecCloseStream(snd_stream_t *stream) +{ + // check if input is valid + if(!stream) + { + return; + } + + // let the OGG codec cleanup its stuff + ov_clear((OggVorbis_File *) stream->ptr); + + // free the OGG codec control struct + Z_Free(stream->ptr); + + // close the stream + S_CodecUtilClose(&stream); +} + +/* +================= +S_OGG_CodecReadStream +================= +*/ +int S_OGG_CodecReadStream(snd_stream_t *stream, int bytes, void *buffer) +{ + // buffer handling + int bytesRead, bytesLeft, c; + char *bufPtr; + + // Bitstream for the decoder + int BS = 0; + + // big endian machines want their samples in big endian order + int IsBigEndian = 0; + +# ifdef Q3_BIG_ENDIAN + IsBigEndian = 1; +# endif // Q3_BIG_ENDIAN + + // check if input is valid + if(!(stream && buffer)) + { + return 0; + } + + if(bytes <= 0) + { + return 0; + } + + bytesRead = 0; + bytesLeft = bytes; + bufPtr = (char*)buffer; + + // cycle until we have the requested or all available bytes read + while(-1) + { + // read some bytes from the OGG codec + c = ov_read((OggVorbis_File *) stream->ptr, bufPtr, bytesLeft, IsBigEndian, OGG_SAMPLEWIDTH, 1, &BS); + + // no more bytes are left + if(c <= 0) + { + break; + } + + bytesRead += c; + bytesLeft -= c; + bufPtr += c; + + // we have enough bytes + if(bytesLeft <= 0) + { + break; + } + } + + return bytesRead; +} + +/* +===================================================================== +S_OGG_CodecLoad + +We handle S_OGG_CodecLoad as a special case of the streaming functions +where we read the whole stream at once. +====================================================================== +*/ +void *S_OGG_CodecLoad(const char *filename, snd_info_t *info) +{ + snd_stream_t *stream; + byte *buffer; + int bytesRead; + + // check if input is valid + if(!(filename && info)) + { + return NULL; + } + + // open the file as a stream + stream = S_OGG_CodecOpenStream(filename); + if(!stream) + { + return NULL; + } + + // copy over the info + info->rate = stream->info.rate; + info->width = stream->info.width; + info->channels = stream->info.channels; + info->samples = stream->info.samples; + info->size = stream->info.size; + info->dataofs = stream->info.dataofs; + + // allocate a buffer + // this buffer must be free-ed by the caller of this function + buffer = (byte*)Hunk_AllocateTempMemory(info->size); + if(!buffer) + { + S_OGG_CodecCloseStream(stream); + + return NULL; + } + + // fill the buffer + bytesRead = S_OGG_CodecReadStream(stream, info->size, buffer); + + // we don't even have read a single byte + if(bytesRead <= 0) + { + Hunk_FreeTempMemory(buffer); + S_OGG_CodecCloseStream(stream); + + return NULL; + } + + S_OGG_CodecCloseStream(stream); + + return buffer; +} + +#endif // USE_CODEC_VORBIS diff --git a/src/client/snd_codec_opus.cpp b/src/client/snd_codec_opus.cpp new file mode 100644 index 0000000..a8ffba5 --- /dev/null +++ b/src/client/snd_codec_opus.cpp @@ -0,0 +1,452 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2005 Stuart Dalton (badcdev@gmail.com) +Copyright (C) 2005-2006 Joerg Dietrich <dietrich_joerg@gmx.de> +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +// Ogg Opus support is enabled by this define +#ifdef USE_CODEC_OPUS + +// includes for the Q3 sound system +#include "client.h" + +// includes for the Ogg Opus codec +#include <opusfile.h> + +#include <cerrno> + +#include "snd_codec.h" + +// samples are 16 bit +#define OPUS_SAMPLEWIDTH 2 + +// Q3 Ogg Opus codec +snd_codec_t opus_codec = +{ + "opus", + S_OggOpus_CodecLoad, + S_OggOpus_CodecOpenStream, + S_OggOpus_CodecReadStream, + S_OggOpus_CodecCloseStream, + NULL +}; + +// callbacks for opusfile + +// fread() replacement +int S_OggOpus_Callback_read(void *datasource, unsigned char *ptr, int size ) +{ + snd_stream_t *stream; + int bytesRead = 0; + + // check if input is valid + if(!ptr) + { + errno = EFAULT; + return -1; + } + + if(!size) + { + // It's not an error, caller just wants zero bytes! + errno = 0; + return 0; + } + + if (size < 0) + { + errno = EINVAL; + return -1; + } + + if(!datasource) + { + errno = EBADF; + return -1; + } + + // we use a snd_stream_t in the generic pointer to pass around + stream = (snd_stream_t *) datasource; + + // read it with the Q3 function FS_Read() + bytesRead = FS_Read(ptr, size, stream->file); + + // update the file position + stream->pos += bytesRead; + + return bytesRead; +} + +// fseek() replacement +int S_OggOpus_Callback_seek(void *datasource, opus_int64 offset, int whence) +{ + snd_stream_t *stream; + int retVal = 0; + + // check if input is valid + if(!datasource) + { + errno = EBADF; + return -1; + } + + // snd_stream_t in the generic pointer + stream = (snd_stream_t *) datasource; + + // we must map the whence to its Q3 counterpart + switch(whence) + { + case SEEK_SET : + { + // set the file position in the actual file with the Q3 function + retVal = FS_Seek(stream->file, (long) offset, FS_SEEK_SET); + + // something has gone wrong, so we return here + if(retVal < 0) + { + return retVal; + } + + // keep track of file position + stream->pos = (int) offset; + break; + } + + case SEEK_CUR : + { + // set the file position in the actual file with the Q3 function + retVal = FS_Seek(stream->file, (long) offset, FS_SEEK_CUR); + + // something has gone wrong, so we return here + if(retVal < 0) + { + return retVal; + } + + // keep track of file position + stream->pos += (int) offset; + break; + } + + case SEEK_END : + { + // set the file position in the actual file with the Q3 function + retVal = FS_Seek(stream->file, (long) offset, FS_SEEK_END); + + // something has gone wrong, so we return here + if(retVal < 0) + { + return retVal; + } + + // keep track of file position + stream->pos = stream->length + (int) offset; + break; + } + + default : + { + // unknown whence, so we return an error + errno = EINVAL; + return -1; + } + } + + // stream->pos shouldn't be smaller than zero or bigger than the filesize + stream->pos = (stream->pos < 0) ? 0 : stream->pos; + stream->pos = (stream->pos > stream->length) ? stream->length : stream->pos; + + return 0; +} + +// fclose() replacement +int S_OggOpus_Callback_close(void *datasource) +{ + // we do nothing here and close all things manually in S_OggOpus_CodecCloseStream() + return 0; +} + +// ftell() replacement +opus_int64 S_OggOpus_Callback_tell(void *datasource) +{ + snd_stream_t *stream; + + // check if input is valid + if(!datasource) + { + errno = EBADF; + return -1; + } + + // snd_stream_t in the generic pointer + stream = (snd_stream_t *) datasource; + + return (opus_int64) FS_FTell(stream->file); +} + +// the callback structure +const OpusFileCallbacks S_OggOpus_Callbacks = +{ + &S_OggOpus_Callback_read, + &S_OggOpus_Callback_seek, + &S_OggOpus_Callback_tell, + &S_OggOpus_Callback_close +}; + +/* +================= +S_OggOpus_CodecOpenStream +================= +*/ +snd_stream_t *S_OggOpus_CodecOpenStream(const char *filename) +{ + snd_stream_t *stream; + + // Opus codec control structure + OggOpusFile *of; + + // some variables used to get informations about the file + const OpusHead *opusInfo; + ogg_int64_t numSamples; + + // check if input is valid + if(!filename) + { + return NULL; + } + + // Open the stream + stream = S_CodecUtilOpen(filename, &opus_codec); + if(!stream) + { + return NULL; + } + + // open the codec with our callbacks and stream as the generic pointer + of = op_open_callbacks(stream, &S_OggOpus_Callbacks, NULL, 0, NULL ); + if (!of) + { + S_CodecUtilClose(&stream); + + return NULL; + } + + // the stream must be seekable + if(!op_seekable(of)) + { + op_free(of); + + S_CodecUtilClose(&stream); + + return NULL; + } + + // get the info about channels and rate + opusInfo = op_head(of, -1); + if(!opusInfo) + { + op_free(of); + + S_CodecUtilClose(&stream); + + return NULL; + } + + if(opusInfo->stream_count != 1) + { + op_free(of); + + S_CodecUtilClose(&stream); + + Com_Printf("Only Ogg Opus files with one stream are support\n"); + return NULL; + } + + if(opusInfo->channel_count != 1 && opusInfo->channel_count != 2) + { + op_free(of); + + S_CodecUtilClose(&stream); + + Com_Printf("Only mono and stereo Ogg Opus files are supported\n"); + return NULL; + } + + // get the number of sample-frames in the file + numSamples = op_pcm_total(of, -1); + + // fill in the info-structure in the stream + stream->info.rate = 48000; + stream->info.width = OPUS_SAMPLEWIDTH; + stream->info.channels = opusInfo->channel_count; + stream->info.samples = numSamples; + stream->info.size = stream->info.samples * stream->info.channels * stream->info.width; + stream->info.dataofs = 0; + + // We use stream->pos for the file pointer in the compressed ogg file + stream->pos = 0; + + // We use the generic pointer in stream for the opus codec control structure + stream->ptr = of; + + return stream; +} + +/* +================= +S_OggOpus_CodecCloseStream +================= +*/ +void S_OggOpus_CodecCloseStream(snd_stream_t *stream) +{ + // check if input is valid + if(!stream) + { + return; + } + + // let the opus codec cleanup its stuff + op_free((OggOpusFile *) stream->ptr); + + // close the stream + S_CodecUtilClose(&stream); +} + +/* +================= +S_OggOpus_CodecReadStream +================= +*/ +int S_OggOpus_CodecReadStream(snd_stream_t *stream, int bytes, void *buffer) +{ + // buffer handling + int samplesRead, samplesLeft, c; + opus_int16 *bufPtr; + + // check if input is valid + if(!(stream && buffer)) + { + return 0; + } + + if(bytes <= 0) + { + return 0; + } + + samplesRead = 0; + samplesLeft = bytes / stream->info.channels / stream->info.width; + bufPtr = (opus_int16*)buffer; + + if(samplesLeft <= 0) + { + return 0; + } + + // cycle until we have the requested or all available bytes read + while(-1) + { + // read some samples from the opus codec + c = op_read((OggOpusFile *) stream->ptr, bufPtr + samplesRead * stream->info.channels, samplesLeft * stream->info.channels, NULL); + + // no more samples are left + if(c <= 0) + { + break; + } + + samplesRead += c; + samplesLeft -= c; + + // we have enough samples + if(samplesLeft <= 0) + { + break; + } + } + + return samplesRead * stream->info.channels * stream->info.width; +} + +/* +===================================================================== +S_OggOpus_CodecLoad + +We handle S_OggOpus_CodecLoad as a special case of the streaming functions +where we read the whole stream at once. +====================================================================== +*/ +void *S_OggOpus_CodecLoad(const char *filename, snd_info_t *info) +{ + snd_stream_t *stream; + byte *buffer; + int bytesRead; + + // check if input is valid + if(!(filename && info)) + { + return NULL; + } + + // open the file as a stream + stream = S_OggOpus_CodecOpenStream(filename); + if(!stream) + { + return NULL; + } + + // copy over the info + info->rate = stream->info.rate; + info->width = stream->info.width; + info->channels = stream->info.channels; + info->samples = stream->info.samples; + info->size = stream->info.size; + info->dataofs = stream->info.dataofs; + + // allocate a buffer + // this buffer must be free-ed by the caller of this function + buffer = (byte*)Hunk_AllocateTempMemory(info->size); + if(!buffer) + { + S_OggOpus_CodecCloseStream(stream); + + return NULL; + } + + // fill the buffer + bytesRead = S_OggOpus_CodecReadStream(stream, info->size, buffer); + + // we don't even have read a single byte + if(bytesRead <= 0) + { + Hunk_FreeTempMemory(buffer); + S_OggOpus_CodecCloseStream(stream); + + return NULL; + } + + S_OggOpus_CodecCloseStream(stream); + + return buffer; +} + +#endif // USE_CODEC_OPUS diff --git a/src/client/snd_codec_wav.cpp b/src/client/snd_codec_wav.cpp new file mode 100644 index 0000000..a4016e2 --- /dev/null +++ b/src/client/snd_codec_wav.cpp @@ -0,0 +1,293 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2005 Stuart Dalton (badcdev@gmail.com) +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#include "client.h" +#include "snd_codec.h" + +/* +================= +FGetLittleLong +================= +*/ +static int FGetLittleLong( fileHandle_t f ) { + int v; + + FS_Read( &v, sizeof(v), f ); + + return LittleLong( v); +} + +/* +================= +FGetLittleShort +================= +*/ +static short FGetLittleShort( fileHandle_t f ) { + short v; + + FS_Read( &v, sizeof(v), f ); + + return LittleShort( v); +} + +/* +================= +S_ReadChunkInfo +================= +*/ +static int S_ReadChunkInfo(fileHandle_t f, char *name) +{ + int len, r; + + name[4] = 0; + + r = FS_Read(name, 4, f); + if(r != 4) + return -1; + + len = FGetLittleLong(f); + if( len < 0 ) { + Com_Printf( S_COLOR_YELLOW "WARNING: Negative chunk length\n" ); + return -1; + } + + return len; +} + +/* +================= +S_FindRIFFChunk + +Returns the length of the data in the chunk, or -1 if not found +================= +*/ +static int S_FindRIFFChunk( fileHandle_t f, const char *chunk ) { + char name[5]; + int len; + + while( ( len = S_ReadChunkInfo(f, name) ) >= 0 ) + { + // If this is the right chunk, return + if( !Q_strncmp( name, chunk, 4 ) ) + return len; + + len = PAD( len, 2 ); + + // Not the right chunk - skip it + FS_Seek( f, len, FS_SEEK_CUR ); + } + + return -1; +} + +/* +================= +S_ByteSwapRawSamples +================= +*/ +static void S_ByteSwapRawSamples( int samples, int width, int s_channels, const byte *data ) { + int i; + + if ( width != 2 ) { + return; + } + if ( LittleShort( 256 ) == 256 ) { + return; + } + + if ( s_channels == 2 ) { + samples <<= 1; + } + for ( i = 0 ; i < samples ; i++ ) { + ((short *)data)[i] = LittleShort( ((short *)data)[i] ); + } +} + +/* +================= +S_ReadRIFFHeader +================= +*/ +static bool S_ReadRIFFHeader(fileHandle_t file, snd_info_t *info) +{ + char dump[16]; + int bits; + int fmtlen = 0; + + // skip the riff wav header + FS_Read(dump, 12, file); + + // Scan for the format chunk + if((fmtlen = S_FindRIFFChunk(file, "fmt ")) < 0) + { + Com_Printf( S_COLOR_RED "ERROR: Couldn't find \"fmt\" chunk\n"); + return false; + } + + // Save the parameters + FGetLittleShort(file); // wav_format + info->channels = FGetLittleShort(file); + info->rate = FGetLittleLong(file); + FGetLittleLong(file); + FGetLittleShort(file); + bits = FGetLittleShort(file); + + if( bits < 8 ) + { + Com_Printf( S_COLOR_RED "ERROR: Less than 8 bit sound is not supported\n"); + return false; + } + + info->width = bits / 8; + info->dataofs = 0; + + // Skip the rest of the format chunk if required + if(fmtlen > 16) + { + fmtlen -= 16; + FS_Seek( file, fmtlen, FS_SEEK_CUR ); + } + + // Scan for the data chunk + if( (info->size = S_FindRIFFChunk(file, "data")) < 0) + { + Com_Printf( S_COLOR_RED "ERROR: Couldn't find \"data\" chunk\n"); + return false; + } + info->samples = (info->size / info->width) / info->channels; + + return true; +} + +// WAV codec +snd_codec_t wav_codec = +{ + "wav", + S_WAV_CodecLoad, + S_WAV_CodecOpenStream, + S_WAV_CodecReadStream, + S_WAV_CodecCloseStream, + NULL +}; + +/* +================= +S_WAV_CodecLoad +================= +*/ +void *S_WAV_CodecLoad(const char *filename, snd_info_t *info) +{ + fileHandle_t file; + void *buffer; + + // Try to open the file + FS_FOpenFileRead(filename, &file, true); + if(!file) + { + return NULL; + } + + // Read the RIFF header + if(!S_ReadRIFFHeader(file, info)) + { + FS_FCloseFile(file); + Com_Printf( S_COLOR_RED "ERROR: Incorrect/unsupported format in \"%s\"\n", + filename); + return NULL; + } + + // Allocate some memory + buffer = Hunk_AllocateTempMemory(info->size); + if(!buffer) + { + FS_FCloseFile(file); + Com_Printf( S_COLOR_RED "ERROR: Out of memory reading \"%s\"\n", + filename); + return NULL; + } + + // Read, byteswap + FS_Read(buffer, info->size, file); + S_ByteSwapRawSamples(info->samples, info->width, info->channels, (byte *)buffer); + + // Close and return + FS_FCloseFile(file); + return buffer; +} + +/* +================= +S_WAV_CodecOpenStream +================= +*/ +snd_stream_t *S_WAV_CodecOpenStream(const char *filename) +{ + snd_stream_t *rv; + + // Open + rv = S_CodecUtilOpen(filename, &wav_codec); + if(!rv) + return NULL; + + // Read the RIFF header + if(!S_ReadRIFFHeader(rv->file, &rv->info)) + { + S_CodecUtilClose(&rv); + return NULL; + } + + return rv; +} + +/* +================= +S_WAV_CodecCloseStream +================= +*/ +void S_WAV_CodecCloseStream(snd_stream_t *stream) +{ + S_CodecUtilClose(&stream); +} + +/* +================= +S_WAV_CodecReadStream +================= +*/ +int S_WAV_CodecReadStream(snd_stream_t *stream, int bytes, void *buffer) +{ + int remaining = stream->info.size - stream->pos; + int samples; + + if(remaining <= 0) + return 0; + if(bytes > remaining) + bytes = remaining; + stream->pos += bytes; + samples = (bytes / stream->info.width) / stream->info.channels; + FS_Read(buffer, bytes, stream->file); + S_ByteSwapRawSamples(samples, stream->info.width, stream->info.channels, (byte*)buffer); + return bytes; +} diff --git a/src/client/snd_dma.cpp b/src/client/snd_dma.cpp new file mode 100644 index 0000000..bef2b69 --- /dev/null +++ b/src/client/snd_dma.cpp @@ -0,0 +1,1644 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +/***************************************************************************** + * name: snd_dma.c + * + * desc: main control for any streaming sound output device + * + * $Archive: /MissionPack/code/client/snd_dma.c $ + * + *****************************************************************************/ + +#include "snd_local.h" +#include "snd_codec.h" +#include "client.h" + +void S_Update_( void ); +void S_Base_StopAllSounds(void); +void S_Base_StopBackgroundTrack( void ); + +snd_stream_t *s_backgroundStream = NULL; +static char s_backgroundLoop[MAX_QPATH]; +//static char s_backgroundMusic[MAX_QPATH]; //TTimo: unused + + +// ======================================================================= +// Internal sound data & structures +// ======================================================================= + +// only begin attenuating sound volumes when outside the FULLVOLUME range +#define SOUND_FULLVOLUME 80 + +#define SOUND_ATTENUATE 0.0008f + +channel_t s_channels[MAX_CHANNELS]; +channel_t loop_channels[MAX_CHANNELS]; +int numLoopChannels; + +static bool s_soundStarted; +static bool s_soundMuted; + +dma_t dma; + +static int listener_number; +static vec3_t listener_origin; +static vec3_t listener_axis[3]; + +int s_soundtime; // sample PAIRS +int s_paintedtime; // sample PAIRS + +// MAX_SFX may be larger than MAX_SOUNDS because +// of custom player sounds +#define MAX_SFX 4096 +sfx_t s_knownSfx[MAX_SFX]; +int s_numSfx = 0; + +#define LOOP_HASH 128 +static sfx_t *sfxHash[LOOP_HASH]; + +cvar_t *s_testsound; +cvar_t *s_show; +cvar_t *s_mixahead; +cvar_t *s_mixPreStep; + +static loopSound_t loopSounds[MAX_GENTITIES]; +static channel_t *freelist = NULL; + +int s_rawend[MAX_RAW_STREAMS]; +portable_samplepair_t s_rawsamples[MAX_RAW_STREAMS][MAX_RAW_SAMPLES]; + + +// ==================================================================== +// User-setable variables +// ==================================================================== + + +void S_Base_SoundInfo(void) { + Com_Printf("----- Sound Info -----\n" ); + if (!s_soundStarted) { + Com_Printf ("sound system not started\n"); + } else { + Com_Printf("%5d stereo\n", dma.channels - 1); + Com_Printf("%5d samples\n", dma.samples); + Com_Printf("%5d samplebits\n", dma.samplebits); + Com_Printf("%5d submission_chunk\n", dma.submission_chunk); + Com_Printf("%5d speed\n", dma.speed); + Com_Printf("%p dma buffer\n", dma.buffer); + if ( s_backgroundStream ) { + Com_Printf("Background file: %s\n", s_backgroundLoop ); + } else { + Com_Printf("No background file.\n" ); + } + + } + Com_Printf("----------------------\n" ); +} + + +#ifdef USE_VOIP +static +void S_Base_StartCapture( void ) +{ + // !!! FIXME: write me. +} + +static +int S_Base_AvailableCaptureSamples( void ) +{ + // !!! FIXME: write me. + return 0; +} + +static +void S_Base_Capture( int samples, byte *data ) +{ + // !!! FIXME: write me. +} + +static +void S_Base_StopCapture( void ) +{ + // !!! FIXME: write me. +} + +static +void S_Base_MasterGain( float val ) +{ + // !!! FIXME: write me. +} +#endif + + + +/* +================= +S_Base_SoundList +================= +*/ +void S_Base_SoundList( void ) { + int i; + sfx_t *sfx; + int size, total; + char type[4][16]; + char mem[2][16]; + + strcpy(type[0], "16bit"); + strcpy(type[1], "adpcm"); + strcpy(type[2], "daub4"); + strcpy(type[3], "mulaw"); + strcpy(mem[0], "paged out"); + strcpy(mem[1], "resident "); + total = 0; + for (sfx=s_knownSfx, i=0 ; i<s_numSfx ; i++, sfx++) { + size = sfx->soundLength; + total += size; + Com_Printf("%6i[%s] : %s[%s]\n", size, type[sfx->soundCompressionMethod], + sfx->soundName, mem[sfx->inMemory] ); + } + Com_Printf ("Total resident: %i\n", total); + S_DisplayFreeMemory(); +} + + + +void S_ChannelFree(channel_t *v) { + v->thesfx = NULL; + *(channel_t **)v = freelist; + freelist = (channel_t*)v; +} + +channel_t* S_ChannelMalloc( void ) { + channel_t *v; + if (freelist == NULL) { + return NULL; + } + v = freelist; + freelist = *(channel_t **)freelist; + v->allocTime = Com_Milliseconds(); + return v; +} + +void S_ChannelSetup( void ) { + channel_t *p, *q; + + // clear all the sounds so they don't + ::memset( s_channels, 0, sizeof( s_channels ) ); + + p = s_channels;; + q = p + MAX_CHANNELS; + while (--q > p) { + *(channel_t **)q = q-1; + } + + *(channel_t **)q = NULL; + freelist = p + MAX_CHANNELS - 1; + Com_DPrintf("Channel memory manager started\n"); +} + + + +// ======================================================================= +// Load a sound +// ======================================================================= + +/* +================ +return a hash value for the sfx name +================ +*/ +static long S_HashSFXName(const char *name) { + int i; + long hash; + char letter; + + hash = 0; + i = 0; + while (name[i] != '\0') { + letter = tolower(name[i]); + if (letter =='.') break; // don't include extension + if (letter =='\\') letter = '/'; // damn path names + hash+=(long)(letter)*(i+119); + i++; + } + hash &= (LOOP_HASH-1); + return hash; +} + +/* +================== +S_FindName + +Will allocate a new sfx if it isn't found +================== +*/ +static sfx_t *S_FindName( const char *name ) { + int i; + int hash; + + sfx_t *sfx; + + if (!name) { + Com_Error(ERR_FATAL, "Sound name is NULL"); + } + + if (!name[0]) { + Com_Printf( S_COLOR_YELLOW "WARNING: Sound name is empty\n" ); + return NULL; + } + + if (strlen(name) >= MAX_QPATH) { + Com_Printf( S_COLOR_YELLOW "WARNING: Sound name is too long: %s\n", name ); + return NULL; + } + + if (name[0] == '*') { + Com_Printf( S_COLOR_YELLOW "WARNING: Tried to load player sound directly: %s\n", name ); + return NULL; + } + + hash = S_HashSFXName(name); + + sfx = sfxHash[hash]; + // see if already loaded + while (sfx) { + if (!Q_stricmp(sfx->soundName, name) ) { + return sfx; + } + sfx = sfx->next; + } + + // find a free sfx + for (i=0 ; i < s_numSfx ; i++) { + if (!s_knownSfx[i].soundName[0]) { + break; + } + } + + if (i == s_numSfx) { + if (s_numSfx == MAX_SFX) { + Com_Error (ERR_FATAL, "S_FindName: out of sfx_t"); + } + s_numSfx++; + } + + sfx = &s_knownSfx[i]; + ::memset (sfx, 0, sizeof(*sfx)); + strcpy (sfx->soundName, name); + + sfx->next = sfxHash[hash]; + sfxHash[hash] = sfx; + + return sfx; +} + +/* +================= +S_DefaultSound +================= +*/ +void S_DefaultSound( sfx_t *sfx ) { + + int i; + + sfx->soundLength = 512; + sfx->soundData = SND_malloc(); + sfx->soundData->next = NULL; + + + for ( i = 0 ; i < sfx->soundLength ; i++ ) { + sfx->soundData->sndChunk[i] = i; + } +} + +/* +=================== +S_DisableSounds + +Disables sounds until the next S_BeginRegistration. +This is called when the hunk is cleared and the sounds +are no longer valid. +=================== +*/ +void S_Base_DisableSounds( void ) { + S_Base_StopAllSounds(); + s_soundMuted = true; +} + +/* +================== +S_RegisterSound + +Creates a default buzz sound if the file can't be loaded +================== +*/ +sfxHandle_t S_Base_RegisterSound( const char *name, bool compressed ) { + sfx_t *sfx; + + compressed = false; + if (!s_soundStarted) { + return 0; + } + + sfx = S_FindName( name ); + if ( !sfx ) { + return 0; + } + + if ( sfx->soundData ) { + if ( sfx->defaultSound ) { + Com_Printf( S_COLOR_YELLOW "WARNING: could not find %s - using default\n", sfx->soundName ); + return 0; + } + return sfx - s_knownSfx; + } + + sfx->inMemory = false; + sfx->soundCompressed = compressed; + + S_memoryLoad(sfx); + + if ( sfx->defaultSound ) { + Com_Printf( S_COLOR_YELLOW "WARNING: could not find %s - using default\n", sfx->soundName ); + return 0; + } + + return sfx - s_knownSfx; +} + +/* +================== +S_Base_SoundDuration +================== +*/ +static int S_Base_SoundDuration( sfxHandle_t handle ) { + if ( handle < 0 || handle >= s_numSfx ) { + Com_Printf( S_COLOR_YELLOW "S_Base_SoundDuration: handle %i out of range\n", handle ); + return 0; + } + return s_knownSfx[ handle ].duration; +} + + + +/* +===================== +S_BeginRegistration + +===================== +*/ +void S_Base_BeginRegistration( void ) { + s_soundMuted = false; // we can play again + + if (s_numSfx == 0) { + SND_setup(); + + ::memset(s_knownSfx, '\0', sizeof(s_knownSfx)); + ::memset(sfxHash, '\0', sizeof(sfx_t *) * LOOP_HASH); + + S_Base_RegisterSound("sound/feedback/hit.wav", false); // changed to a sound in baseq3 + } +} + +void S_memoryLoad(sfx_t *sfx) { + // load the sound file + if ( !S_LoadSound ( sfx ) ) { +// Com_Printf( S_COLOR_YELLOW "WARNING: couldn't load sound: %s\n", sfx->soundName ); + sfx->defaultSound = true; + } + sfx->inMemory = true; +} + +//============================================================================= + +/* +================= +S_SpatializeOrigin + +Used for spatializing s_channels +================= +*/ +void S_SpatializeOrigin (vec3_t origin, int master_vol, int *left_vol, int *right_vol) +{ + vec_t dot; + vec_t dist; + vec_t lscale, rscale, scale; + vec3_t source_vec; + vec3_t vec; + + const float dist_mult = SOUND_ATTENUATE; + + // calculate stereo seperation and distance attenuation + VectorSubtract(origin, listener_origin, source_vec); + + dist = VectorNormalize(source_vec); + dist -= SOUND_FULLVOLUME; + if (dist < 0) + dist = 0; // close enough to be at full volume + dist *= dist_mult; // different attenuation levels + + VectorRotate( source_vec, listener_axis, vec ); + + dot = -vec[1]; + + if (dma.channels == 1) + { // no attenuation = no spatialization + rscale = 1.0; + lscale = 1.0; + } + else + { + rscale = 0.5 * (1.0 + dot); + lscale = 0.5 * (1.0 - dot); + if ( rscale < 0 ) { + rscale = 0; + } + if ( lscale < 0 ) { + lscale = 0; + } + } + + // add in distance effect + scale = (1.0 - dist) * rscale; + *right_vol = (master_vol * scale); + if (*right_vol < 0) + *right_vol = 0; + + scale = (1.0 - dist) * lscale; + *left_vol = (master_vol * scale); + if (*left_vol < 0) + *left_vol = 0; +} + +// ======================================================================= +// Start a sound effect +// ======================================================================= + +/* +================= +S_Base_HearingThroughEntity + +Also see S_AL_HearingThroughEntity +================= +*/ +static bool S_Base_HearingThroughEntity( int entityNum, vec3_t origin ) +{ + float distanceSq; + vec3_t sorigin; + + if (origin) + VectorCopy(origin, sorigin); + else + VectorCopy(loopSounds[entityNum].origin, sorigin); + + if( listener_number == entityNum ) + { + // This is an outrageous hack to detect + // whether or not the player is rendering in third person or not. We can't + // ask the renderer because the renderer has no notion of entities and we + // can't ask cgame since that would involve changing the API and hence mod + // compatibility. I don't think there is any way around this, but I'll leave + // the FIXME just in case anyone has a bright idea. + distanceSq = DistanceSquared( + sorigin, + listener_origin ); + + if( distanceSq > THIRD_PERSON_THRESHOLD_SQ ) + return false; //we're the player, but third person + else + return true; //we're the player + } + else + return false; //not the player +} + +/* +==================== +S_Base_StartSoundEx + +Validates the parms and ques the sound up +if origin is NULL, the sound will be dynamically sourced from the entity +Entchannel 0 will never override a playing sound +==================== +*/ +static void S_Base_StartSoundEx( vec3_t origin, int entityNum, int entchannel, sfxHandle_t sfxHandle, bool localSound ) { + channel_t *ch; + sfx_t *sfx; + int i, oldest, chosen, time; + int inplay, allowed; + bool fullVolume; + + if ( !s_soundStarted || s_soundMuted ) { + return; + } + + if ( !origin && ( entityNum < 0 || entityNum >= MAX_GENTITIES ) ) { + Com_Error( ERR_DROP, "S_StartSound: bad entitynum %i", entityNum ); + } + + if ( sfxHandle < 0 || sfxHandle >= s_numSfx ) { + Com_Printf( S_COLOR_YELLOW "S_StartSound: handle %i out of range\n", sfxHandle ); + return; + } + + sfx = &s_knownSfx[ sfxHandle ]; + + if (sfx->inMemory == false) { + S_memoryLoad(sfx); + } + + if ( s_show->integer == 1 ) { + Com_Printf( "%i : %s\n", s_paintedtime, sfx->soundName ); + } + + time = Com_Milliseconds(); + +// Com_Printf("playing %s\n", sfx->soundName); + // pick a channel to play on + + allowed = 4; + if (entityNum == listener_number) { + allowed = 8; + } + + fullVolume = false; + if (localSound || S_Base_HearingThroughEntity(entityNum, origin)) { + fullVolume = true; + } + + ch = s_channels; + inplay = 0; + for ( i = 0; i < MAX_CHANNELS ; i++, ch++ ) { + if (ch->entnum == entityNum && ch->thesfx == sfx) { + if (time - ch->allocTime < 50) { +// if (Cvar_VariableValue( "cg_showmiss" )) { +// Com_Printf("double sound start\n"); +// } + return; + } + inplay++; + } + } + + if (inplay>allowed) { + return; + } + + sfx->lastTimeUsed = time; + + ch = S_ChannelMalloc(); // entityNum, entchannel); + if (!ch) { + ch = s_channels; + + oldest = sfx->lastTimeUsed; + chosen = -1; + for ( i = 0 ; i < MAX_CHANNELS ; i++, ch++ ) { + if (ch->entnum != listener_number && ch->entnum == entityNum && ch->allocTime<oldest && ch->entchannel != CHAN_ANNOUNCER) { + oldest = ch->allocTime; + chosen = i; + } + } + if (chosen == -1) { + ch = s_channels; + for ( i = 0 ; i < MAX_CHANNELS ; i++, ch++ ) { + if (ch->entnum != listener_number && ch->allocTime<oldest && ch->entchannel != CHAN_ANNOUNCER) { + oldest = ch->allocTime; + chosen = i; + } + } + if (chosen == -1) { + ch = s_channels; + if (ch->entnum == listener_number) { + for ( i = 0 ; i < MAX_CHANNELS ; i++, ch++ ) { + if (ch->allocTime<oldest) { + oldest = ch->allocTime; + chosen = i; + } + } + } + if (chosen == -1) { + Com_DPrintf("dropping sound\n"); + return; + } + } + } + ch = &s_channels[chosen]; + ch->allocTime = sfx->lastTimeUsed; + } + + if (origin) { + VectorCopy (origin, ch->origin); + ch->fixed_origin = true; + } else { + ch->fixed_origin = false; + } + + ch->master_vol = 127; + ch->entnum = entityNum; + ch->thesfx = sfx; + ch->startSample = START_SAMPLE_IMMEDIATE; + ch->entchannel = entchannel; + ch->leftvol = ch->master_vol; // these will get calced at next spatialize + ch->rightvol = ch->master_vol; // unless the game isn't running + ch->doppler = false; + ch->fullVolume = fullVolume; +} + +/* +==================== +S_StartSound + +if origin is NULL, the sound will be dynamically sourced from the entity +==================== +*/ +void S_Base_StartSound( vec3_t origin, int entityNum, int entchannel, sfxHandle_t sfxHandle ) { + S_Base_StartSoundEx( origin, entityNum, entchannel, sfxHandle, false ); +} + +/* +================== +S_StartLocalSound +================== +*/ +void S_Base_StartLocalSound( sfxHandle_t sfxHandle, int channelNum ) { + if ( !s_soundStarted || s_soundMuted ) { + return; + } + + if ( sfxHandle < 0 || sfxHandle >= s_numSfx ) { + Com_Printf( S_COLOR_YELLOW "S_StartLocalSound: handle %i out of range\n", sfxHandle ); + return; + } + + S_Base_StartSoundEx( NULL, listener_number, channelNum, sfxHandle, true ); +} + + +/* +================== +S_ClearSoundBuffer + +If we are about to perform file access, clear the buffer +so sound doesn't stutter. +================== +*/ +void S_Base_ClearSoundBuffer( void ) { + int clear; + + if (!s_soundStarted) + return; + + // stop looping sounds + ::memset(loopSounds, 0, MAX_GENTITIES*sizeof(loopSound_t)); + ::memset(loop_channels, 0, MAX_CHANNELS*sizeof(channel_t)); + numLoopChannels = 0; + + S_ChannelSetup(); + + ::memset(s_rawend, '\0', sizeof (s_rawend)); + + if (dma.samplebits == 8) + clear = 0x80; + else + clear = 0; + + SNDDMA_BeginPainting (); + if (dma.buffer) + ::memset(dma.buffer, clear, dma.samples * dma.samplebits/8); + SNDDMA_Submit (); +} + +/* +================== +S_StopAllSounds +================== +*/ +void S_Base_StopAllSounds(void) { + if ( !s_soundStarted ) { + return; + } + + // stop the background music + S_Base_StopBackgroundTrack(); + + S_Base_ClearSoundBuffer (); +} + +/* +============================================================== + +continuous looping sounds are added each frame + +============================================================== +*/ + +void S_Base_StopLoopingSound(int entityNum) { + loopSounds[entityNum].active = false; +// loopSounds[entityNum].sfx = 0; + loopSounds[entityNum].kill = false; +} + +/* +================== +S_ClearLoopingSounds + +================== +*/ +void S_Base_ClearLoopingSounds( bool killall ) +{ + int i; + for ( i = 0 ; i < MAX_GENTITIES ; i++) { + if (killall || loopSounds[i].kill == true || (loopSounds[i].sfx && loopSounds[i].sfx->soundLength == 0)) { + S_Base_StopLoopingSound(i); + } + } + numLoopChannels = 0; +} + +/* +================== +S_AddLoopingSound + +Called during entity generation for a frame +Include velocity in case I get around to doing doppler... +================== +*/ +void S_Base_AddLoopingSound( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfxHandle ) { + sfx_t *sfx; + + if ( !s_soundStarted || s_soundMuted ) { + return; + } + + if ( sfxHandle < 0 || sfxHandle >= s_numSfx ) { + Com_Printf( S_COLOR_YELLOW "S_AddLoopingSound: handle %i out of range\n", sfxHandle ); + return; + } + + sfx = &s_knownSfx[ sfxHandle ]; + + if (sfx->inMemory == false) { + S_memoryLoad(sfx); + } + + if ( !sfx->soundLength ) { + Com_Error( ERR_DROP, "%s has length 0", sfx->soundName ); + } + + VectorCopy( origin, loopSounds[entityNum].origin ); + VectorCopy( velocity, loopSounds[entityNum].velocity ); + loopSounds[entityNum].active = true; + loopSounds[entityNum].kill = true; + loopSounds[entityNum].doppler = false; + loopSounds[entityNum].oldDopplerScale = 1.0; + loopSounds[entityNum].dopplerScale = 1.0; + loopSounds[entityNum].sfx = sfx; + + if (s_doppler->integer && VectorLengthSquared(velocity)>0.0) { + vec3_t out; + float lena, lenb; + + loopSounds[entityNum].doppler = true; + lena = DistanceSquared(loopSounds[listener_number].origin, loopSounds[entityNum].origin); + VectorAdd(loopSounds[entityNum].origin, loopSounds[entityNum].velocity, out); + lenb = DistanceSquared(loopSounds[listener_number].origin, out); + if ((loopSounds[entityNum].framenum+1) != cls.framecount) { + loopSounds[entityNum].oldDopplerScale = 1.0; + } else { + loopSounds[entityNum].oldDopplerScale = loopSounds[entityNum].dopplerScale; + } + loopSounds[entityNum].dopplerScale = lenb/(lena*100); + if (loopSounds[entityNum].dopplerScale<=1.0) { + loopSounds[entityNum].doppler = false; // don't bother doing the math + } else if (loopSounds[entityNum].dopplerScale>MAX_DOPPLER_SCALE) { + loopSounds[entityNum].dopplerScale = MAX_DOPPLER_SCALE; + } + } + + loopSounds[entityNum].framenum = cls.framecount; +} + +/* +================== +S_AddLoopingSound + +Called during entity generation for a frame +Include velocity in case I get around to doing doppler... +================== +*/ +void S_Base_AddRealLoopingSound( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfxHandle ) { + sfx_t *sfx; + + if ( !s_soundStarted || s_soundMuted ) { + return; + } + + if ( sfxHandle < 0 || sfxHandle >= s_numSfx ) { + Com_Printf( S_COLOR_YELLOW "S_AddRealLoopingSound: handle %i out of range\n", sfxHandle ); + return; + } + + sfx = &s_knownSfx[ sfxHandle ]; + + if (sfx->inMemory == false) { + S_memoryLoad(sfx); + } + + if ( !sfx->soundLength ) { + Com_Error( ERR_DROP, "%s has length 0", sfx->soundName ); + } + VectorCopy( origin, loopSounds[entityNum].origin ); + VectorCopy( velocity, loopSounds[entityNum].velocity ); + loopSounds[entityNum].sfx = sfx; + loopSounds[entityNum].active = true; + loopSounds[entityNum].kill = false; + loopSounds[entityNum].doppler = false; +} + + + +/* +================== +S_AddLoopSounds + +Spatialize all of the looping sounds. +All sounds are on the same cycle, so any duplicates can just +sum up the channel multipliers. +================== +*/ +void S_AddLoopSounds (void) { + int i, j, time; + int left_total, right_total, left, right; + channel_t *ch; + loopSound_t *loop, *loop2; + static int loopFrame; + + + numLoopChannels = 0; + + time = Com_Milliseconds(); + + loopFrame++; + for ( i = 0 ; i < MAX_GENTITIES ; i++) { + loop = &loopSounds[i]; + if ( !loop->active || loop->mergeFrame == loopFrame ) { + continue; // already merged into an earlier sound + } + + if (loop->kill) { + S_SpatializeOrigin( loop->origin, 127, &left_total, &right_total); // 3d + } else { + S_SpatializeOrigin( loop->origin, 90, &left_total, &right_total); // sphere + } + + loop->sfx->lastTimeUsed = time; + + for (j=(i+1); j< MAX_GENTITIES ; j++) { + loop2 = &loopSounds[j]; + if ( !loop2->active || loop2->doppler || loop2->sfx != loop->sfx) { + continue; + } + loop2->mergeFrame = loopFrame; + + if (loop2->kill) { + S_SpatializeOrigin( loop2->origin, 127, &left, &right); // 3d + } else { + S_SpatializeOrigin( loop2->origin, 90, &left, &right); // sphere + } + + loop2->sfx->lastTimeUsed = time; + left_total += left; + right_total += right; + } + if (left_total == 0 && right_total == 0) { + continue; // not audible + } + + // allocate a channel + ch = &loop_channels[numLoopChannels]; + + if (left_total > 255) { + left_total = 255; + } + if (right_total > 255) { + right_total = 255; + } + + ch->master_vol = 127; + ch->leftvol = left_total; + ch->rightvol = right_total; + ch->thesfx = loop->sfx; + ch->doppler = loop->doppler; + ch->dopplerScale = loop->dopplerScale; + ch->oldDopplerScale = loop->oldDopplerScale; + ch->fullVolume = false; + numLoopChannels++; + if (numLoopChannels == MAX_CHANNELS) { + return; + } + } +} + +//============================================================================= + +/* +================= +S_ByteSwapRawSamples + +If raw data has been loaded in little endien binary form, this must be done. +If raw data was calculated, as with ADPCM, this should not be called. +================= +*/ +void S_ByteSwapRawSamples( int samples, int width, int s_channels, const byte *data ) { + int i; + + if ( width != 2 ) { + return; + } + if ( LittleShort( 256 ) == 256 ) { + return; + } + + if ( s_channels == 2 ) { + samples <<= 1; + } + for ( i = 0 ; i < samples ; i++ ) { + ((short *)data)[i] = LittleShort( ((short *)data)[i] ); + } +} + +/* +============ +S_Base_RawSamples + +Music streaming +============ +*/ +void S_Base_RawSamples( int stream, int samples, int rate, int width, int s_channels, const byte *data, float volume, int entityNum) +{ + int i; + int src, dst; + float scale; + int intVolumeLeft, intVolumeRight; + portable_samplepair_t *rawsamples; + + if ( !s_soundStarted || s_soundMuted ) { + return; + } + + if ( (stream < 0) || (stream >= MAX_RAW_STREAMS) ) { + return; + } + + rawsamples = s_rawsamples[stream]; + + if ( s_muted->integer ) { + intVolumeLeft = intVolumeRight = 0; + } else { + int leftvol, rightvol; + + if ( entityNum >= 0 && entityNum < MAX_GENTITIES ) { + // support spatialized raw streams, e.g. for VoIP + S_SpatializeOrigin( loopSounds[ entityNum ].origin, 256, &leftvol, &rightvol ); + } else { + leftvol = rightvol = 256; + } + + intVolumeLeft = leftvol * volume * s_volume->value; + intVolumeRight = rightvol * volume * s_volume->value; + } + + if ( s_rawend[stream] < s_soundtime ) { + Com_DPrintf( "S_Base_RawSamples: resetting minimum: %i < %i\n", s_rawend[stream], s_soundtime ); + s_rawend[stream] = s_soundtime; + } + + scale = (float)rate / dma.speed; + +//Com_Printf ("%i < %i < %i\n", s_soundtime, s_paintedtime, s_rawend[stream]); + if (s_channels == 2 && width == 2) + { + if (scale == 1.0) + { // optimized case + for (i=0 ; i<samples ; i++) + { + dst = s_rawend[stream]&(MAX_RAW_SAMPLES-1); + s_rawend[stream]++; + rawsamples[dst].left = ((short *)data)[i*2] * intVolumeLeft; + rawsamples[dst].right = ((short *)data)[i*2+1] * intVolumeRight; + } + } + else + { + for (i=0 ; ; i++) + { + src = i*scale; + if (src >= samples) + break; + dst = s_rawend[stream]&(MAX_RAW_SAMPLES-1); + s_rawend[stream]++; + rawsamples[dst].left = ((short *)data)[src*2] * intVolumeLeft; + rawsamples[dst].right = ((short *)data)[src*2+1] * intVolumeRight; + } + } + } + else if (s_channels == 1 && width == 2) + { + for (i=0 ; ; i++) + { + src = i*scale; + if (src >= samples) + break; + dst = s_rawend[stream]&(MAX_RAW_SAMPLES-1); + s_rawend[stream]++; + rawsamples[dst].left = ((short *)data)[src] * intVolumeLeft; + rawsamples[dst].right = ((short *)data)[src] * intVolumeRight; + } + } + else if (s_channels == 2 && width == 1) + { + intVolumeLeft *= 256; + intVolumeRight *= 256; + + for (i=0 ; ; i++) + { + src = i*scale; + if (src >= samples) + break; + dst = s_rawend[stream]&(MAX_RAW_SAMPLES-1); + s_rawend[stream]++; + rawsamples[dst].left = ((char *)data)[src*2] * intVolumeLeft; + rawsamples[dst].right = ((char *)data)[src*2+1] * intVolumeRight; + } + } + else if (s_channels == 1 && width == 1) + { + intVolumeLeft *= 256; + intVolumeRight *= 256; + + for (i=0 ; ; i++) + { + src = i*scale; + if (src >= samples) + break; + dst = s_rawend[stream]&(MAX_RAW_SAMPLES-1); + s_rawend[stream]++; + rawsamples[dst].left = (((byte *)data)[src]-128) * intVolumeLeft; + rawsamples[dst].right = (((byte *)data)[src]-128) * intVolumeRight; + } + } + + if ( s_rawend[stream] > s_soundtime + MAX_RAW_SAMPLES ) { + Com_DPrintf( "S_Base_RawSamples: overflowed %i > %i\n", s_rawend[stream], s_soundtime ); + } +} + +//============================================================================= + +/* +===================== +S_UpdateEntityPosition + +let the sound system know where an entity currently is +====================== +*/ +void S_Base_UpdateEntityPosition( int entityNum, const vec3_t origin ) { + if ( entityNum < 0 || entityNum >= MAX_GENTITIES ) { + Com_Error( ERR_DROP, "S_UpdateEntityPosition: bad entitynum %i", entityNum ); + } + VectorCopy( origin, loopSounds[entityNum].origin ); +} + + +/* +============ +S_Respatialize + +Change the volumes of all the playing sounds for changes in their positions +============ +*/ +void S_Base_Respatialize( int entityNum, const vec3_t head, vec3_t axis[3], int inwater ) { + int i; + channel_t *ch; + vec3_t origin; + + if ( !s_soundStarted || s_soundMuted ) { + return; + } + + listener_number = entityNum; + VectorCopy(head, listener_origin); + VectorCopy(axis[0], listener_axis[0]); + VectorCopy(axis[1], listener_axis[1]); + VectorCopy(axis[2], listener_axis[2]); + + // update spatialization for dynamic sounds + ch = s_channels; + for ( i = 0 ; i < MAX_CHANNELS ; i++, ch++ ) { + if ( !ch->thesfx ) { + continue; + } + // local and first person sounds will always be full volume + if (ch->fullVolume) { + ch->leftvol = ch->master_vol; + ch->rightvol = ch->master_vol; + } else { + if (ch->fixed_origin) { + VectorCopy( ch->origin, origin ); + } else { + VectorCopy( loopSounds[ ch->entnum ].origin, origin ); + } + + S_SpatializeOrigin (origin, ch->master_vol, &ch->leftvol, &ch->rightvol); + } + } + + // add loopsounds + S_AddLoopSounds (); +} + + +/* +======================== +S_ScanChannelStarts + +Returns true if any new sounds were started since the last mix +======================== +*/ +bool S_ScanChannelStarts( void ) { + channel_t *ch; + int i; + bool newSamples; + + newSamples = false; + ch = s_channels; + + for (i=0; i<MAX_CHANNELS ; i++, ch++) { + if ( !ch->thesfx ) { + continue; + } + // if this channel was just started this frame, + // set the sample count to it begins mixing + // into the very first sample + if ( ch->startSample == START_SAMPLE_IMMEDIATE ) { + ch->startSample = s_paintedtime; + newSamples = true; + continue; + } + + // if it is completely finished by now, clear it + if ( ch->startSample + (ch->thesfx->soundLength) <= s_paintedtime ) { + S_ChannelFree(ch); + } + } + + return newSamples; +} + +/* +============ +S_Update + +Called once each time through the main loop +============ +*/ +void S_Base_Update( void ) { + int i; + int total; + channel_t *ch; + + if ( !s_soundStarted || s_soundMuted ) { +// Com_DPrintf ("not started or muted\n"); + return; + } + + // + // debugging output + // + if ( s_show->integer == 2 ) { + total = 0; + ch = s_channels; + for (i=0 ; i<MAX_CHANNELS; i++, ch++) { + if (ch->thesfx && (ch->leftvol || ch->rightvol) ) { + Com_Printf ("%d %d %s\n", ch->leftvol, ch->rightvol, ch->thesfx->soundName); + total++; + } + } + + Com_Printf ("----(%i)---- painted: %i\n", total, s_paintedtime); + } + + // add raw data from streamed samples + S_UpdateBackgroundTrack(); + + // mix some sound + S_Update_(); +} + +void S_GetSoundtime(void) +{ + int samplepos; + static int buffers; + static int oldsamplepos; + int fullsamples; + + fullsamples = dma.samples / dma.channels; + + if( CL_VideoRecording( ) ) + { + float fps = MIN(cl_aviFrameRate->value, 1000.0f); + float frameDuration = MAX(dma.speed / fps, 1.0f) + clc.aviSoundFrameRemainder; + + int msec = (int)frameDuration; + s_soundtime += msec; + clc.aviSoundFrameRemainder = frameDuration - msec; + + return; + } + + // it is possible to miscount buffers if it has wrapped twice between + // calls to S_Update. Oh well. + samplepos = SNDDMA_GetDMAPos(); + if (samplepos < oldsamplepos) + { + buffers++; // buffer wrapped + + if (s_paintedtime > 0x40000000) + { // time to chop things off to avoid 32 bit limits + buffers = 0; + s_paintedtime = fullsamples; + S_Base_StopAllSounds (); + } + } + oldsamplepos = samplepos; + + s_soundtime = buffers*fullsamples + samplepos/dma.channels; + +#if 0 +// check to make sure that we haven't overshot + if (s_paintedtime < s_soundtime) + { + Com_DPrintf ("S_Update_ : overflow\n"); + s_paintedtime = s_soundtime; + } +#endif + + if ( dma.submission_chunk < 256 ) { + s_paintedtime = s_soundtime + s_mixPreStep->value * dma.speed; + } else { + s_paintedtime = s_soundtime + dma.submission_chunk; + } +} + + +void S_Update_(void) { + unsigned endtime; + int samps; + static float lastTime = 0.0f; + float ma, op; + float thisTime, sane; + static int ot = -1; + + if ( !s_soundStarted || s_soundMuted ) { + return; + } + + thisTime = Com_Milliseconds(); + + // Updates s_soundtime + S_GetSoundtime(); + + if (s_soundtime == ot) { + return; + } + ot = s_soundtime; + + // clear any sound effects that end before the current time, + // and start any new sounds + S_ScanChannelStarts(); + + sane = thisTime - lastTime; + if (sane<11) { + sane = 11; // 85hz + } + + ma = s_mixahead->value * dma.speed; + op = s_mixPreStep->value + sane*dma.speed*0.01; + + if (op < ma) { + ma = op; + } + + // mix ahead of current position + endtime = s_soundtime + ma; + + // mix to an even submission block size + endtime = (endtime + dma.submission_chunk-1) + & ~(dma.submission_chunk-1); + + // never mix more than the complete buffer + samps = dma.samples >> (dma.channels-1); + if (endtime - s_soundtime > samps) + endtime = s_soundtime + samps; + + + + SNDDMA_BeginPainting (); + + S_PaintChannels (endtime); + + SNDDMA_Submit (); + + lastTime = thisTime; +} + + + +/* +=============================================================================== + +background music functions + +=============================================================================== +*/ + +/* +====================== +S_StopBackgroundTrack +====================== +*/ +void S_Base_StopBackgroundTrack( void ) { + if(!s_backgroundStream) + return; + S_CodecCloseStream(s_backgroundStream); + s_backgroundStream = NULL; + s_rawend[0] = 0; +} + +/* +====================== +S_OpenBackgroundStream +====================== +*/ +static void S_OpenBackgroundStream( const char *filename ) { + // close the background track, but DON'T reset s_rawend + // if restarting the same back ground track + if(s_backgroundStream) + { + S_CodecCloseStream(s_backgroundStream); + s_backgroundStream = NULL; + } + + // Open stream + s_backgroundStream = S_CodecOpenStream(filename); + if(!s_backgroundStream) { + Com_Printf( S_COLOR_YELLOW "WARNING: couldn't open music file %s\n", filename ); + return; + } + + if(s_backgroundStream->info.channels != 2 || s_backgroundStream->info.rate != 22050) { + Com_Printf(S_COLOR_YELLOW "WARNING: music file %s is not 22k stereo\n", filename ); + } +} + +/* +====================== +S_StartBackgroundTrack +====================== +*/ +void S_Base_StartBackgroundTrack( const char *intro, const char *loop ){ + if ( !intro ) { + intro = ""; + } + if ( !loop || !loop[0] ) { + loop = intro; + } + Com_DPrintf( "S_StartBackgroundTrack( %s, %s )\n", intro, loop ); + + if(!*intro) + { + S_Base_StopBackgroundTrack(); + return; + } + + Q_strncpyz( s_backgroundLoop, loop, sizeof( s_backgroundLoop ) ); + + S_OpenBackgroundStream( intro ); +} + +/* +====================== +S_UpdateBackgroundTrack +====================== +*/ +void S_UpdateBackgroundTrack( void ) { + int bufferSamples; + int fileSamples; + byte raw[30000]; // just enough to fit in a mac stack frame + int fileBytes; + int r; + + if(!s_backgroundStream) { + return; + } + + // don't bother playing anything if musicvolume is 0 + if ( s_musicVolume->value <= 0 ) { + return; + } + + // see how many samples should be copied into the raw buffer + if ( s_rawend[0] < s_soundtime ) { + s_rawend[0] = s_soundtime; + } + + while ( s_rawend[0] < s_soundtime + MAX_RAW_SAMPLES ) { + bufferSamples = MAX_RAW_SAMPLES - (s_rawend[0] - s_soundtime); + + // decide how much data needs to be read from the file + fileSamples = bufferSamples * s_backgroundStream->info.rate / dma.speed; + + if (!fileSamples) + return; + + // our max buffer size + fileBytes = fileSamples * (s_backgroundStream->info.width * s_backgroundStream->info.channels); + if ( fileBytes > sizeof(raw) ) { + fileBytes = sizeof(raw); + fileSamples = fileBytes / (s_backgroundStream->info.width * s_backgroundStream->info.channels); + } + + // Read + r = S_CodecReadStream(s_backgroundStream, fileBytes, raw); + if(r < fileBytes) + { + fileSamples = r / (s_backgroundStream->info.width * s_backgroundStream->info.channels); + } + + if(r > 0) + { + // add to raw buffer + S_Base_RawSamples(0, fileSamples, s_backgroundStream->info.rate, + s_backgroundStream->info.width, s_backgroundStream->info.channels, raw, s_musicVolume->value, -1); + } + else + { + // loop + if(s_backgroundLoop[0]) + { + S_OpenBackgroundStream( s_backgroundLoop ); + if(!s_backgroundStream) + return; + } + else + { + S_Base_StopBackgroundTrack(); + return; + } + } + + } +} + + +/* +====================== +S_FreeOldestSound +====================== +*/ + +void S_FreeOldestSound( void ) { + int i, oldest, used; + sfx_t *sfx; + sndBuffer *buffer, *nbuffer; + + oldest = Com_Milliseconds(); + used = 0; + + for (i=1 ; i < s_numSfx ; i++) { + sfx = &s_knownSfx[i]; + if (sfx->inMemory && sfx->lastTimeUsed<oldest) { + used = i; + oldest = sfx->lastTimeUsed; + } + } + + sfx = &s_knownSfx[used]; + + Com_DPrintf("S_FreeOldestSound: freeing sound %s\n", sfx->soundName); + + buffer = sfx->soundData; + while(buffer != NULL) { + nbuffer = buffer->next; + SND_free(buffer); + buffer = nbuffer; + } + sfx->inMemory = false; + sfx->soundData = NULL; +} + +// ======================================================================= +// Shutdown sound engine +// ======================================================================= + +void S_Base_Shutdown( void ) { + if ( !s_soundStarted ) { + return; + } + + SNDDMA_Shutdown(); + SND_shutdown(); + + s_soundStarted = false; + s_numSfx = 0; + + Cmd_RemoveCommand("s_info"); +} + +/* +================ +S_Init +================ +*/ +bool S_Base_Init( soundInterface_t *si ) +{ + + if( !si ) { + return false; + } + + s_mixahead = Cvar_Get ("s_mixahead", "0.2", CVAR_ARCHIVE); + s_mixPreStep = Cvar_Get ("s_mixPreStep", "0.05", CVAR_ARCHIVE); + s_show = Cvar_Get ("s_show", "0", CVAR_CHEAT); + s_testsound = Cvar_Get ("s_testsound", "0", CVAR_CHEAT); + + bool r = SNDDMA_Init(); + if ( r ) + { + s_soundStarted = true; + s_soundMuted = true; +// s_numSfx = 0; + + ::memset(sfxHash, 0, sizeof(sfx_t *)*LOOP_HASH); + + s_soundtime = 0; + s_paintedtime = 0; + + S_Base_StopAllSounds( ); + } else { + return false; + } + + si->Shutdown = S_Base_Shutdown; + si->StartSound = S_Base_StartSound; + si->StartLocalSound = S_Base_StartLocalSound; + si->StartBackgroundTrack = S_Base_StartBackgroundTrack; + si->StopBackgroundTrack = S_Base_StopBackgroundTrack; + si->RawSamples = S_Base_RawSamples; + si->StopAllSounds = S_Base_StopAllSounds; + si->ClearLoopingSounds = S_Base_ClearLoopingSounds; + si->AddLoopingSound = S_Base_AddLoopingSound; + si->AddRealLoopingSound = S_Base_AddRealLoopingSound; + si->StopLoopingSound = S_Base_StopLoopingSound; + si->Respatialize = S_Base_Respatialize; + si->UpdateEntityPosition = S_Base_UpdateEntityPosition; + si->Update = S_Base_Update; + si->DisableSounds = S_Base_DisableSounds; + si->BeginRegistration = S_Base_BeginRegistration; + si->RegisterSound = S_Base_RegisterSound; + si->SoundDuration = S_Base_SoundDuration; + si->ClearSoundBuffer = S_Base_ClearSoundBuffer; + si->SoundInfo = S_Base_SoundInfo; + si->SoundList = S_Base_SoundList; + +#ifdef USE_VOIP + si->StartCapture = S_Base_StartCapture; + si->AvailableCaptureSamples = S_Base_AvailableCaptureSamples; + si->Capture = S_Base_Capture; + si->StopCapture = S_Base_StopCapture; + si->MasterGain = S_Base_MasterGain; +#endif + + return true; +} diff --git a/src/client/snd_local.h b/src/client/snd_local.h new file mode 100644 index 0000000..38f26c4 --- /dev/null +++ b/src/client/snd_local.h @@ -0,0 +1,267 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ +// snd_local.h -- private sound definations + +#ifndef _SND_LOCAL_H +#define _SND_LOCAL_H + +#include "qcommon/q_shared.h" +#include "qcommon/qcommon.h" + +#include "snd_public.h" + +typedef struct cvar_s cvar_t; + +#define PAINTBUFFER_SIZE 4096 // this is in samples + +#define SND_CHUNK_SIZE 1024 // samples +#define SND_CHUNK_SIZE_FLOAT (SND_CHUNK_SIZE/2) // floats +#define SND_CHUNK_SIZE_BYTE (SND_CHUNK_SIZE*2) // floats + +typedef struct { + int left; // the final values will be clamped to +/- 0x00ffff00 and shifted down + int right; +} portable_samplepair_t; + +typedef struct adpcm_state { + short sample; /* Previous output value */ + char index; /* Index into stepsize table */ +} adpcm_state_t; + +typedef struct sndBuffer_s { + short sndChunk[SND_CHUNK_SIZE]; + struct sndBuffer_s *next; + int size; + adpcm_state_t adpcm; +} sndBuffer; + +typedef struct sfx_s { + sndBuffer *soundData; + bool defaultSound; // couldn't be loaded, so use buzz + bool inMemory; // not in Memory + bool soundCompressed; // not in Memory + int soundCompressionMethod; + int soundLength; + int soundChannels; + char soundName[MAX_QPATH]; + int lastTimeUsed; + int duration; + struct sfx_s *next; +} sfx_t; + +typedef struct { + int channels; + int samples; // mono samples in buffer + int submission_chunk; // don't mix less than this # + int samplebits; + int speed; + byte *buffer; +} dma_t; + +#define START_SAMPLE_IMMEDIATE 0x7fffffff + +#define MAX_DOPPLER_SCALE 50.0f //arbitrary + +#define THIRD_PERSON_THRESHOLD_SQ (48.0f*48.0f) + +typedef struct loopSound_s { + vec3_t origin; + vec3_t velocity; + sfx_t *sfx; + int mergeFrame; + bool active; + bool kill; + bool doppler; + float dopplerScale; + float oldDopplerScale; + int framenum; +} loopSound_t; + +typedef struct +{ + sfx_t *thesfx; // sfx structure + int allocTime; + int startSample; // START_SAMPLE_IMMEDIATE = set immediately on next mix + int entnum; // to allow overriding a specific sound + int entchannel; // to allow overriding a specific sound + int leftvol; // 0-255 volume after spatialization + int rightvol; // 0-255 volume after spatialization + int master_vol; // 0-255 volume before spatialization + float dopplerScale; + float oldDopplerScale; + vec3_t origin; // only use if fixed_origin is set + bool fixed_origin; // use origin instead of fetching entnum's origin + bool doppler; + bool fullVolume; +} channel_t; + + +#define WAV_FORMAT_PCM 1 + + +typedef struct { + int format; + int rate; + int width; + int channels; + int samples; + int dataofs; // chunk starts this many bytes from file start +} wavinfo_t; + +// Interface between Q3 sound "api" and the sound backend +typedef struct +{ + void (*Shutdown)(void); + void (*StartSound)( vec3_t origin, int entnum, int entchannel, sfxHandle_t sfx ); + void (*StartLocalSound)( sfxHandle_t sfx, int channelNum ); + void (*StartBackgroundTrack)( const char *intro, const char *loop ); + void (*StopBackgroundTrack)( void ); + void (*RawSamples)(int stream, int samples, int rate, int width, int channels, const byte *data, float volume, int entityNum); + void (*StopAllSounds)( void ); + void (*ClearLoopingSounds)( bool killall ); + void (*AddLoopingSound)( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfx ); + void (*AddRealLoopingSound)( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfx ); + void (*StopLoopingSound)(int entityNum ); + void (*Respatialize)( int entityNum, const vec3_t origin, vec3_t axis[3], int inwater ); + void (*UpdateEntityPosition)( int entityNum, const vec3_t origin ); + void (*Update)( void ); + void (*DisableSounds)( void ); + void (*BeginRegistration)( void ); + sfxHandle_t (*RegisterSound)( const char *sample, bool compressed ); + int (*SoundDuration)( sfxHandle_t handle ); + void (*ClearSoundBuffer)( void ); + void (*SoundInfo)( void ); + void (*SoundList)( void ); +#ifdef USE_VOIP + void (*StartCapture)( void ); + int (*AvailableCaptureSamples)( void ); + void (*Capture)( int samples, byte *data ); + void (*StopCapture)( void ); + void (*MasterGain)( float gain ); +#endif +} soundInterface_t; + + +/* +==================================================================== + + SYSTEM SPECIFIC FUNCTIONS + +==================================================================== +*/ + +// initializes cycling through a DMA buffer and returns information on it +bool SNDDMA_Init(void); + +// gets the current DMA position +int SNDDMA_GetDMAPos(void); + +// shutdown the DMA xfer. +void SNDDMA_Shutdown(void); + +void SNDDMA_BeginPainting (void); + +void SNDDMA_Submit(void); + +//==================================================================== + +#define MAX_CHANNELS 96 + +extern channel_t s_channels[MAX_CHANNELS]; +extern channel_t loop_channels[MAX_CHANNELS]; +extern int numLoopChannels; + +extern int s_paintedtime; +extern vec3_t listener_forward; +extern vec3_t listener_right; +extern vec3_t listener_up; +extern dma_t dma; + +#define MAX_RAW_SAMPLES 16384 +#define MAX_RAW_STREAMS (MAX_CLIENTS * 2 + 1) +extern portable_samplepair_t s_rawsamples[MAX_RAW_STREAMS][MAX_RAW_SAMPLES]; +extern int s_rawend[MAX_RAW_STREAMS]; + +extern cvar_t *s_volume; +extern cvar_t *s_musicVolume; +extern cvar_t *s_muted; +extern cvar_t *s_doppler; + +extern cvar_t *s_testsound; + +bool S_LoadSound( sfx_t *sfx ); + +void SND_free(sndBuffer *v); +sndBuffer* SND_malloc( void ); +void SND_setup( void ); +void SND_shutdown(void); + +void S_PaintChannels(int endtime); + +void S_memoryLoad(sfx_t *sfx); + +// spatializes a channel +void S_Spatialize(channel_t *ch); + +// adpcm functions +int S_AdpcmMemoryNeeded( const wavinfo_t *info ); +void S_AdpcmEncodeSound( sfx_t *sfx, short *samples ); +void S_AdpcmGetSamples(sndBuffer *chunk, short *to); + +// wavelet function + +#define SENTINEL_MULAW_ZERO_RUN 127 +#define SENTINEL_MULAW_FOUR_BIT_RUN 126 + +void S_FreeOldestSound( void ); + +#define NXStream byte + +void encodeWavelet(sfx_t *sfx, short *packets); +void decodeWavelet( sndBuffer *stream, short *packets); + +void encodeMuLaw( sfx_t *sfx, short *packets); +extern short mulawToShort[256]; + +extern short *sfxScratchBuffer; +extern sfx_t *sfxScratchPointer; +extern int sfxScratchIndex; + +bool S_Base_Init( soundInterface_t *si ); + +// OpenAL stuff +typedef enum +{ + SRCPRI_AMBIENT = 0, // Ambient sound effects + SRCPRI_ENTITY, // Entity sound effects + SRCPRI_ONESHOT, // One-shot sounds + SRCPRI_LOCAL, // Local sounds + SRCPRI_STREAM // Streams (music, cutscenes) +} alSrcPriority_t; + +typedef int srcHandle_t; + +bool S_AL_Init( soundInterface_t *si ); + +#endif diff --git a/src/client/snd_main.cpp b/src/client/snd_main.cpp new file mode 100644 index 0000000..a6873f1 --- /dev/null +++ b/src/client/snd_main.cpp @@ -0,0 +1,566 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2005 Stuart Dalton (badcdev@gmail.com) +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#include "client.h" +#include "snd_codec.h" +#include "snd_local.h" +#include "snd_public.h" + +cvar_t *s_volume; +cvar_t *s_muted; +cvar_t *s_musicVolume; +cvar_t *s_doppler; +cvar_t *s_backend; +cvar_t *s_muteWhenMinimized; +cvar_t *s_muteWhenUnfocused; + +static soundInterface_t si; + +/* +================= +S_ValidateInterface +================= +*/ +static bool S_ValidSoundInterface( soundInterface_t *si ) +{ + if( !si->Shutdown ) return false; + if( !si->StartSound ) return false; + if( !si->StartLocalSound ) return false; + if( !si->StartBackgroundTrack ) return false; + if( !si->StopBackgroundTrack ) return false; + if( !si->RawSamples ) return false; + if( !si->StopAllSounds ) return false; + if( !si->ClearLoopingSounds ) return false; + if( !si->AddLoopingSound ) return false; + if( !si->AddRealLoopingSound ) return false; + if( !si->StopLoopingSound ) return false; + if( !si->Respatialize ) return false; + if( !si->UpdateEntityPosition ) return false; + if( !si->Update ) return false; + if( !si->DisableSounds ) return false; + if( !si->BeginRegistration ) return false; + if( !si->RegisterSound ) return false; + if( !si->SoundDuration ) return false; + if( !si->ClearSoundBuffer ) return false; + if( !si->SoundInfo ) return false; + if( !si->SoundList ) return false; + +#ifdef USE_VOIP + if( !si->StartCapture ) return false; + if( !si->AvailableCaptureSamples ) return false; + if( !si->Capture ) return false; + if( !si->StopCapture ) return false; + if( !si->MasterGain ) return false; +#endif + + return true; +} + +/* +================= +S_StartSound +================= +*/ +void S_StartSound( vec3_t origin, int entnum, int entchannel, sfxHandle_t sfx ) +{ + if( si.StartSound ) { + si.StartSound( origin, entnum, entchannel, sfx ); + } +} + +/* +================= +S_StartLocalSound +================= +*/ +void S_StartLocalSound( sfxHandle_t sfx, int channelNum ) +{ + if( si.StartLocalSound ) { + si.StartLocalSound( sfx, channelNum ); + } +} + +/* +================= +S_StartBackgroundTrack +================= +*/ +void S_StartBackgroundTrack( const char *intro, const char *loop ) +{ + if( si.StartBackgroundTrack ) { + si.StartBackgroundTrack( intro, loop ); + } +} + +/* +================= +S_StopBackgroundTrack +================= +*/ +void S_StopBackgroundTrack( void ) +{ + if( si.StopBackgroundTrack ) { + si.StopBackgroundTrack( ); + } +} + +/* +================= +S_RawSamples +================= +*/ +void S_RawSamples (int stream, int samples, int rate, int width, int channels, + const byte *data, float volume, int entityNum) +{ + if(si.RawSamples) + si.RawSamples(stream, samples, rate, width, channels, data, volume, entityNum); +} + +/* +================= +S_StopAllSounds +================= +*/ +void S_StopAllSounds( void ) +{ + if( si.StopAllSounds ) { + si.StopAllSounds( ); + } +} + +/* +================= +S_ClearLoopingSounds +================= +*/ +void S_ClearLoopingSounds( bool killall ) +{ + if( si.ClearLoopingSounds ) { + si.ClearLoopingSounds( killall ); + } +} + +/* +================= +S_AddLoopingSound +================= +*/ +void S_AddLoopingSound( int entityNum, const vec3_t origin, + const vec3_t velocity, sfxHandle_t sfx ) +{ + if( si.AddLoopingSound ) { + si.AddLoopingSound( entityNum, origin, velocity, sfx ); + } +} + +/* +================= +S_AddRealLoopingSound +================= +*/ +void S_AddRealLoopingSound( int entityNum, const vec3_t origin, + const vec3_t velocity, sfxHandle_t sfx ) +{ + if( si.AddRealLoopingSound ) { + si.AddRealLoopingSound( entityNum, origin, velocity, sfx ); + } +} + +/* +================= +S_StopLoopingSound +================= +*/ +void S_StopLoopingSound( int entityNum ) +{ + if( si.StopLoopingSound ) { + si.StopLoopingSound( entityNum ); + } +} + +/* +================= +S_Respatialize +================= +*/ +void S_Respatialize( int entityNum, const vec3_t origin, + vec3_t axis[3], int inwater ) +{ + if( si.Respatialize ) { + si.Respatialize( entityNum, origin, axis, inwater ); + } +} + +/* +================= +S_UpdateEntityPosition +================= +*/ +void S_UpdateEntityPosition( int entityNum, const vec3_t origin ) +{ + if( si.UpdateEntityPosition ) { + si.UpdateEntityPosition( entityNum, origin ); + } +} + +/* +================= +S_Update +================= +*/ +void S_Update( void ) +{ + if(s_muted->integer) + { + if(!(s_muteWhenMinimized->integer && com_minimized->integer) && + !(s_muteWhenUnfocused->integer && com_unfocused->integer)) + { + s_muted->integer = false; + s_muted->modified = true; + } + } + else + { + if((s_muteWhenMinimized->integer && com_minimized->integer) || + (s_muteWhenUnfocused->integer && com_unfocused->integer)) + { + s_muted->integer = true; + s_muted->modified = true; + } + } + + if( si.Update ) { + si.Update( ); + } +} + +/* +================= +S_DisableSounds +================= +*/ +void S_DisableSounds( void ) +{ + if( si.DisableSounds ) { + si.DisableSounds( ); + } +} + +/* +================= +S_BeginRegistration +================= +*/ +void S_BeginRegistration( void ) +{ + if( si.BeginRegistration ) { + si.BeginRegistration( ); + } +} + +/* +================= +S_RegisterSound +================= +*/ +sfxHandle_t S_RegisterSound( const char *sample, bool compressed ) +{ + if( si.RegisterSound ) { + return si.RegisterSound( sample, compressed ); + } else { + return 0; + } +} + +/* +================= +S_SoundDuration +================= +*/ +int S_SoundDuration( sfxHandle_t handle ) +{ + if( si.SoundDuration ) + return si.SoundDuration( handle ); + else + return 0; +} + +/* +================= +S_ClearSoundBuffer +================= +*/ +void S_ClearSoundBuffer( void ) +{ + if( si.ClearSoundBuffer ) { + si.ClearSoundBuffer( ); + } +} + +/* +================= +S_SoundInfo +================= +*/ +void S_SoundInfo( void ) +{ + if( si.SoundInfo ) { + si.SoundInfo( ); + } +} + +/* +================= +S_SoundList +================= +*/ +void S_SoundList( void ) +{ + if( si.SoundList ) { + si.SoundList( ); + } +} + + +#ifdef USE_VOIP +/* +================= +S_StartCapture +================= +*/ +void S_StartCapture( void ) +{ + if( si.StartCapture ) { + si.StartCapture( ); + } +} + +/* +================= +S_AvailableCaptureSamples +================= +*/ +int S_AvailableCaptureSamples( void ) +{ + if( si.AvailableCaptureSamples ) { + return si.AvailableCaptureSamples( ); + } + return 0; +} + +/* +================= +S_Capture +================= +*/ +void S_Capture( int samples, byte *data ) +{ + if( si.Capture ) { + si.Capture( samples, data ); + } +} + +/* +================= +S_StopCapture +================= +*/ +void S_StopCapture( void ) +{ + if( si.StopCapture ) { + si.StopCapture( ); + } +} + +/* +================= +S_MasterGain +================= +*/ +void S_MasterGain( float gain ) +{ + if( si.MasterGain ) { + si.MasterGain( gain ); + } +} +#endif + +//============================================================================= + +/* +================= +S_Play_f +================= +*/ +void S_Play_f( void ) { + int i; + int c; + sfxHandle_t h; + + if( !si.RegisterSound || !si.StartLocalSound ) { + return; + } + + c = Cmd_Argc(); + + if( c < 2 ) { + Com_Printf ("Usage: play <sound filename> [sound filename] [sound filename] ...\n"); + return; + } + + for( i = 1; i < c; i++ ) { + h = si.RegisterSound( Cmd_Argv(i), false ); + + if( h ) { + si.StartLocalSound( h, CHAN_LOCAL_SOUND ); + } + } +} + +/* +================= +S_Music_f +================= +*/ +void S_Music_f( void ) { + int c; + + if( !si.StartBackgroundTrack ) { + return; + } + + c = Cmd_Argc(); + + if ( c == 2 ) { + si.StartBackgroundTrack( Cmd_Argv(1), NULL ); + } else if ( c == 3 ) { + si.StartBackgroundTrack( Cmd_Argv(1), Cmd_Argv(2) ); + } else { + Com_Printf ("Usage: music <musicfile> [loopfile]\n"); + return; + } + +} + +/* +================= +S_Music_f +================= +*/ +void S_StopMusic_f( void ) +{ + if(!si.StopBackgroundTrack) + return; + + si.StopBackgroundTrack(); +} + + +//============================================================================= + +/* +================= +S_Init +================= +*/ +void S_Init( void ) +{ + cvar_t *cv; + bool started = false; + + Com_Printf( "------ Initializing Sound ------\n" ); + + s_volume = Cvar_Get( "s_volume", "0.8", CVAR_ARCHIVE ); + s_musicVolume = Cvar_Get( "s_musicvolume", "0.25", CVAR_ARCHIVE ); + s_muted = Cvar_Get("s_muted", "0", CVAR_ROM); + s_doppler = Cvar_Get( "s_doppler", "1", CVAR_ARCHIVE ); + s_backend = Cvar_Get( "s_backend", "", CVAR_ROM ); + s_muteWhenMinimized = Cvar_Get( "s_muteWhenMinimized", "0", CVAR_ARCHIVE ); + s_muteWhenUnfocused = Cvar_Get( "s_muteWhenUnfocused", "0", CVAR_ARCHIVE ); + + cv = Cvar_Get( "s_initsound", "1", 0 ); + if( !cv->integer ) { + Com_Printf( "Sound disabled.\n" ); + } else { + + S_CodecInit( ); + + Cmd_AddCommand( "play", S_Play_f ); + Cmd_AddCommand( "music", S_Music_f ); + Cmd_AddCommand( "stopmusic", S_StopMusic_f ); + Cmd_AddCommand( "s_list", S_SoundList ); + Cmd_AddCommand( "s_stop", S_StopAllSounds ); + Cmd_AddCommand( "s_info", S_SoundInfo ); + + cv = Cvar_Get( "s_useOpenAL", "0", CVAR_ARCHIVE ); + if( cv->integer ) { + //OpenAL + started = S_AL_Init( &si ); + Cvar_Set( "s_backend", "OpenAL" ); + } + + if( !started ) { + started = S_Base_Init( &si ); + Cvar_Set( "s_backend", "base" ); + } + + if( started ) { + if( !S_ValidSoundInterface( &si ) ) { + Com_Error( ERR_FATAL, "Sound interface invalid" ); + } + + S_SoundInfo( ); + Com_Printf( "Sound initialization successful.\n" ); + } else { + Com_Printf( "Sound initialization failed.\n" ); + } + } + + Com_Printf( "--------------------------------\n"); +} + +/* +================= +S_Shutdown +================= +*/ +void S_Shutdown( void ) +{ + if( si.Shutdown ) + { + si.Shutdown( ); + } + + ::memset( &si, 0, sizeof( soundInterface_t ) ); + + Cmd_RemoveCommand( "play" ); + Cmd_RemoveCommand( "music"); + Cmd_RemoveCommand( "stopmusic"); + Cmd_RemoveCommand( "s_list" ); + Cmd_RemoveCommand( "s_stop" ); + Cmd_RemoveCommand( "s_info" ); + + S_CodecShutdown( ); +} diff --git a/src/client/snd_mem.cpp b/src/client/snd_mem.cpp new file mode 100644 index 0000000..128a471 --- /dev/null +++ b/src/client/snd_mem.cpp @@ -0,0 +1,297 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +/***************************************************************************** + * name: snd_mem.c + * + * desc: sound caching + *****************************************************************************/ + +#include "snd_codec.h" +#include "snd_local.h" + +#include "qcommon/cvar.h" + +#define DEF_COMSOUNDMEGS "8" + +/* +=============================================================================== + +memory management + +=============================================================================== +*/ + +static sndBuffer *buffer = NULL; +static sndBuffer *freelist = NULL; +static int inUse = 0; +static int totalInUse = 0; + +short *sfxScratchBuffer = NULL; +sfx_t *sfxScratchPointer = NULL; +int sfxScratchIndex = 0; + +void SND_free(sndBuffer *v) +{ + *(sndBuffer **)v = freelist; + freelist = (sndBuffer *)v; + inUse += sizeof(sndBuffer); +} + +sndBuffer *SND_malloc(void) +{ + sndBuffer *v; +redo: + if (freelist == NULL) + { + S_FreeOldestSound(); + goto redo; + } + + inUse -= sizeof(sndBuffer); + totalInUse += sizeof(sndBuffer); + + v = freelist; + freelist = *(sndBuffer **)freelist; + v->next = NULL; + + return v; +} + +void SND_setup(void) +{ + cvar_t* cv = Cvar_Get("com_soundMegs", + DEF_COMSOUNDMEGS, CVAR_LATCH | CVAR_ARCHIVE); + int scs = cv->integer * 1536; + + buffer = (sndBuffer*)malloc(scs * sizeof(sndBuffer)); + + // allocate the stack based hunk allocator + sfxScratchBuffer = (short*)malloc(SND_CHUNK_SIZE * sizeof(short) * 4); + sfxScratchPointer = NULL; + + inUse = scs * sizeof(sndBuffer); + sndBuffer* p = buffer; + sndBuffer* q = p + scs; + while (--q > p) + { + *(sndBuffer **)q = q - 1; + } + + *(sndBuffer **)q = NULL; + freelist = p + scs - 1; + + Com_Printf("Sound memory manager started\n"); +} + +void SND_shutdown(void) +{ + free(sfxScratchBuffer); + free(buffer); +} + +/* +================ +ResampleSfx + +resample / decimate to the current source rate +================ +*/ +static int ResampleSfx(sfx_t *sfx, int channels, int inrate, int inwidth, int samples, byte *data, bool compressed) +{ + float stepscale = (float)inrate / dma.speed; // this is usually 0.5, 1, or 2 + int outcount = samples / stepscale; + int fracstep = stepscale * 256 * channels; + sndBuffer* chunk = sfx->soundData; + + int samplefrac = 0; + int srcsample = 0; + + for (int i = 0; i < outcount; i++) + { + srcsample += samplefrac >> 8; + samplefrac &= 255; + samplefrac += fracstep; + + for (int j = 0; j < channels; j++) + { + int sample; + + if (inwidth == 2) + { + sample = ((short *)data)[srcsample + j]; + } + else + { + sample = (int)((unsigned char)(data[srcsample + j]) - 128) << 8; + } + + int part = (i * channels + j) & (SND_CHUNK_SIZE - 1); + if (part == 0) + { + sndBuffer *newchunk = SND_malloc(); + + if (chunk == NULL) + { + sfx->soundData = newchunk; + } + else + { + chunk->next = newchunk; + } + chunk = newchunk; + } + + chunk->sndChunk[part] = sample; + } + } + + return outcount; +} + +/* +================ +ResampleSfxRaw + +resample / decimate to the current source rate +================ +*/ +static int ResampleSfxRaw(short *sfx, int channels, int inrate, int inwidth, int samples, byte *data) +{ + float stepscale = (float)inrate / dma.speed; // this is usually 0.5, 1, or 2 + int outcount = samples / stepscale; + int fracstep = stepscale * 256 * channels; + + int samplefrac = 0; + int srcsample = 0; + + for (int i = 0; i < outcount; i++) + { + srcsample += samplefrac >> 8; + srcsample &= 255; + samplefrac += fracstep; + + for (int j = 0; j < channels; j++) + { + int sample; + if (inwidth == 2) + { + sample = LittleShort(((short *)data)[srcsample + j]); + } + else + { + sample = (int)((unsigned char)(data[srcsample + j]) - 128) << 8; + } + sfx[i * channels + j] = sample; + } + } + return outcount; +} + +//============================================================================= + +/* +============== +S_LoadSound + +The filename may be different than sfx->name in the case +of a forced fallback of a player specific sound +============== +*/ +bool S_LoadSound(sfx_t *sfx) +{ + snd_info_t info; + byte *data = (byte*)S_CodecLoad(sfx->soundName, &info); + if (!data) + { + return false; + } + + int size_per_sec = info.rate * info.channels * info.width; + if (size_per_sec > 0) + { + sfx->duration = (int)(1000.0f * ((double)info.size / size_per_sec)); + } + + if (info.width == 1) + { + Com_DPrintf(S_COLOR_YELLOW "WARNING: %s is a 8 bit audio file\n", + sfx->soundName); + } + + if (info.rate != 22050) + { + Com_DPrintf(S_COLOR_YELLOW "WARNING: %s is not a 22kHz audio file\n", + sfx->soundName); + } + + short* samples = (short*)Hunk_AllocateTempMemory(info.channels * info.samples * sizeof(short) * 2); + + sfx->lastTimeUsed = Com_Milliseconds() + 1; + + // each of these compression schemes works just fine + // but the 16bit quality is much nicer and with a local + // install assured we can rely upon the sound memory + // manager to do the right thing for us and page + // sound in as needed + + if (info.channels == 1 && sfx->soundCompressed == true) + { + sfx->soundCompressionMethod = 1; + sfx->soundData = NULL; + sfx->soundLength = ResampleSfxRaw( + samples, + info.channels, + info.rate, + info.width, + info.samples, + data + info.dataofs); + S_AdpcmEncodeSound(sfx, samples); + } + else + { + sfx->soundCompressionMethod = 0; + sfx->soundData = NULL; + sfx->soundLength = ResampleSfx( + sfx, + info.channels, + info.rate, + info.width, + info.samples, + data + info.dataofs, + false); + } + + sfx->soundChannels = info.channels; + + Hunk_FreeTempMemory(samples); + Hunk_FreeTempMemory(data); + + return true; +} + +void S_DisplayFreeMemory(void) +{ + Com_Printf("%d bytes free sound buffer memory, %d total used\n", inUse, totalInUse); +} diff --git a/src/client/snd_mix.cpp b/src/client/snd_mix.cpp new file mode 100644 index 0000000..4030bc6 --- /dev/null +++ b/src/client/snd_mix.cpp @@ -0,0 +1,792 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ +// snd_mix.c -- portable code to mix sounds for snd_dma.c + +#include "snd_local.h" +#if idppc_altivec && !defined(__APPLE__) +#include <altivec.h> +#endif +#include "client.h" + +static portable_samplepair_t paintbuffer[PAINTBUFFER_SIZE]; +static int snd_vol; + +int* snd_p; +int snd_linear_count; +short* snd_out; + +#undef id386 + +#if !id386 // if configured not to use asm + +void S_WriteLinearBlastStereo16 (void) +{ + int i; + int val; + + for (i=0 ; i<snd_linear_count ; i+=2) + { + val = snd_p[i]>>8; + if (val > 0x7fff) + snd_out[i] = 0x7fff; + else if (val < -32768) + snd_out[i] = -32768; + else + snd_out[i] = val; + + val = snd_p[i+1]>>8; + if (val > 0x7fff) + snd_out[i+1] = 0x7fff; + else if (val < -32768) + snd_out[i+1] = -32768; + else + snd_out[i+1] = val; + } +} +#elif defined(__GNUC__) +// uses snd_mixa.s +void S_WriteLinearBlastStereo16 (void); +#else + +__declspec( naked ) void S_WriteLinearBlastStereo16 (void) +{ + __asm { + + push edi + push ebx + mov ecx,ds:dword ptr[snd_linear_count] + mov ebx,ds:dword ptr[snd_p] + mov edi,ds:dword ptr[snd_out] +LWLBLoopTop: + mov eax,ds:dword ptr[-8+ebx+ecx*4] + sar eax,8 + cmp eax,07FFFh + jg LClampHigh + cmp eax,0FFFF8000h + jnl LClampDone + mov eax,0FFFF8000h + jmp LClampDone +LClampHigh: + mov eax,07FFFh +LClampDone: + mov edx,ds:dword ptr[-4+ebx+ecx*4] + sar edx,8 + cmp edx,07FFFh + jg LClampHigh2 + cmp edx,0FFFF8000h + jnl LClampDone2 + mov edx,0FFFF8000h + jmp LClampDone2 +LClampHigh2: + mov edx,07FFFh +LClampDone2: + shl edx,16 + and eax,0FFFFh + or edx,eax + mov ds:dword ptr[-4+edi+ecx*2],edx + sub ecx,2 + jnz LWLBLoopTop + pop ebx + pop edi + ret + } +} + +#endif + +void S_TransferStereo16 (unsigned long *pbuf, int endtime) +{ + int lpos; + int ls_paintedtime; + + snd_p = (int *) paintbuffer; + ls_paintedtime = s_paintedtime; + + while (ls_paintedtime < endtime) + { + // handle recirculating buffer issues + lpos = ls_paintedtime & ((dma.samples>>1)-1); + + snd_out = (short *) pbuf + (lpos<<1); + + snd_linear_count = (dma.samples>>1) - lpos; + if (ls_paintedtime + snd_linear_count > endtime) + snd_linear_count = endtime - ls_paintedtime; + + snd_linear_count <<= 1; + + // write a linear blast of samples + S_WriteLinearBlastStereo16 (); + + snd_p += snd_linear_count; + ls_paintedtime += (snd_linear_count>>1); + + if( CL_VideoRecording( ) ) + CL_WriteAVIAudioFrame( (byte *)snd_out, snd_linear_count << 1 ); + } +} + +/* +=================== +S_TransferPaintBuffer + +=================== +*/ +void S_TransferPaintBuffer(int endtime) +{ + int out_idx; + int count; + int out_mask; + int *p; + int step; + int val; + unsigned long *pbuf; + + pbuf = (unsigned long *)dma.buffer; + + + if ( s_testsound->integer ) { + int i; + + // write a fixed sine wave + count = (endtime - s_paintedtime); + for (i=0 ; i<count ; i++) + paintbuffer[i].left = paintbuffer[i].right = sin((s_paintedtime+i)*0.1)*20000*256; + } + + + if (dma.samplebits == 16 && dma.channels == 2) + { // optimized case + S_TransferStereo16 (pbuf, endtime); + } + else + { // general case + p = (int *) paintbuffer; + count = (endtime - s_paintedtime) * dma.channels; + out_mask = dma.samples - 1; + out_idx = s_paintedtime * dma.channels & out_mask; + step = 3 - dma.channels; + + if (dma.samplebits == 16) + { + short *out = (short *) pbuf; + while (count--) + { + val = *p >> 8; + p+= step; + if (val > 0x7fff) + val = 0x7fff; + else if (val < -32768) + val = -32768; + out[out_idx] = val; + out_idx = (out_idx + 1) & out_mask; + } + } + else if (dma.samplebits == 8) + { + unsigned char *out = (unsigned char *) pbuf; + while (count--) + { + val = *p >> 8; + p+= step; + if (val > 0x7fff) + val = 0x7fff; + else if (val < -32768) + val = -32768; + out[out_idx] = (val>>8) + 128; + out_idx = (out_idx + 1) & out_mask; + } + } + } +} + + +/* +=============================================================================== + +CHANNEL MIXING + +=============================================================================== +*/ + +#if idppc_altivec +static void S_PaintChannelFrom16_altivec( channel_t *ch, const sfx_t *sc, int count, int sampleOffset, int bufferOffset ) { + int data, aoff, boff; + int leftvol, rightvol; + int i, j; + portable_samplepair_t *samp; + sndBuffer *chunk; + short *samples; + float ooff, fdata[2], fdiv, fleftvol, frightvol; + + if (sc->soundChannels <= 0) { + return; + } + + samp = &paintbuffer[ bufferOffset ]; + + if (ch->doppler) { + sampleOffset = sampleOffset*ch->oldDopplerScale; + } + + if ( sc->soundChannels == 2 ) { + sampleOffset *= sc->soundChannels; + + if ( sampleOffset & 1 ) { + sampleOffset &= ~1; + } + } + + chunk = sc->soundData; + while (sampleOffset>=SND_CHUNK_SIZE) { + chunk = chunk->next; + sampleOffset -= SND_CHUNK_SIZE; + if (!chunk) { + chunk = sc->soundData; + } + } + + if (!ch->doppler || ch->dopplerScale==1.0f) { + vector signed short volume_vec; + vector unsigned int volume_shift; + int vectorCount, samplesLeft, chunkSamplesLeft; + leftvol = ch->leftvol*snd_vol; + rightvol = ch->rightvol*snd_vol; + samples = chunk->sndChunk; + ((short *)&volume_vec)[0] = leftvol; + ((short *)&volume_vec)[1] = leftvol; + ((short *)&volume_vec)[4] = leftvol; + ((short *)&volume_vec)[5] = leftvol; + ((short *)&volume_vec)[2] = rightvol; + ((short *)&volume_vec)[3] = rightvol; + ((short *)&volume_vec)[6] = rightvol; + ((short *)&volume_vec)[7] = rightvol; + volume_shift = vec_splat_u32(8); + i = 0; + + while(i < count) { + /* Try to align destination to 16-byte boundary */ + while(i < count && (((unsigned long)&samp[i] & 0x1f) || ((count-i) < 8) || ((SND_CHUNK_SIZE - sampleOffset) < 8))) { + data = samples[sampleOffset++]; + samp[i].left += (data * leftvol)>>8; + + if ( sc->soundChannels == 2 ) { + data = samples[sampleOffset++]; + } + samp[i].right += (data * rightvol)>>8; + + if (sampleOffset == SND_CHUNK_SIZE) { + chunk = chunk->next; + samples = chunk->sndChunk; + sampleOffset = 0; + } + i++; + } + /* Destination is now aligned. Process as many 8-sample + chunks as we can before we run out of room from the current + sound chunk. We do 8 per loop to avoid extra source data reads. */ + samplesLeft = count - i; + chunkSamplesLeft = SND_CHUNK_SIZE - sampleOffset; + if(samplesLeft > chunkSamplesLeft) + samplesLeft = chunkSamplesLeft; + + vectorCount = samplesLeft / 8; + + if(vectorCount) + { + vector unsigned char tmp; + vector short s0, s1, sampleData0, sampleData1; + vector signed int merge0, merge1; + vector signed int d0, d1, d2, d3; + vector unsigned char samplePermute0 = + VECCONST_UINT8(0, 1, 4, 5, 0, 1, 4, 5, 2, 3, 6, 7, 2, 3, 6, 7); + vector unsigned char samplePermute1 = + VECCONST_UINT8(8, 9, 12, 13, 8, 9, 12, 13, 10, 11, 14, 15, 10, 11, 14, 15); + vector unsigned char loadPermute0, loadPermute1; + + // Rather than permute the vectors after we load them to do the sample + // replication and rearrangement, we permute the alignment vector so + // we do everything in one step below and avoid data shuffling. + tmp = vec_lvsl(0,&samples[sampleOffset]); + loadPermute0 = vec_perm(tmp,tmp,samplePermute0); + loadPermute1 = vec_perm(tmp,tmp,samplePermute1); + + s0 = *(vector short *)&samples[sampleOffset]; + while(vectorCount) + { + /* Load up source (16-bit) sample data */ + s1 = *(vector short *)&samples[sampleOffset+7]; + + /* Load up destination sample data */ + d0 = *(vector signed int *)&samp[i]; + d1 = *(vector signed int *)&samp[i+2]; + d2 = *(vector signed int *)&samp[i+4]; + d3 = *(vector signed int *)&samp[i+6]; + + sampleData0 = vec_perm(s0,s1,loadPermute0); + sampleData1 = vec_perm(s0,s1,loadPermute1); + + merge0 = vec_mule(sampleData0,volume_vec); + merge0 = vec_sra(merge0,volume_shift); /* Shift down to proper range */ + + merge1 = vec_mulo(sampleData0,volume_vec); + merge1 = vec_sra(merge1,volume_shift); + + d0 = vec_add(merge0,d0); + d1 = vec_add(merge1,d1); + + merge0 = vec_mule(sampleData1,volume_vec); + merge0 = vec_sra(merge0,volume_shift); /* Shift down to proper range */ + + merge1 = vec_mulo(sampleData1,volume_vec); + merge1 = vec_sra(merge1,volume_shift); + + d2 = vec_add(merge0,d2); + d3 = vec_add(merge1,d3); + + /* Store destination sample data */ + *(vector signed int *)&samp[i] = d0; + *(vector signed int *)&samp[i+2] = d1; + *(vector signed int *)&samp[i+4] = d2; + *(vector signed int *)&samp[i+6] = d3; + + i += 8; + vectorCount--; + s0 = s1; + sampleOffset += 8; + } + if (sampleOffset == SND_CHUNK_SIZE) { + chunk = chunk->next; + samples = chunk->sndChunk; + sampleOffset = 0; + } + } + } + } else { + fleftvol = ch->leftvol*snd_vol; + frightvol = ch->rightvol*snd_vol; + + ooff = sampleOffset; + samples = chunk->sndChunk; + + for ( i=0 ; i<count ; i++ ) { + + aoff = ooff; + ooff = ooff + ch->dopplerScale * sc->soundChannels; + boff = ooff; + fdata[0] = fdata[1] = 0; + for (j=aoff; j<boff; j += sc->soundChannels) { + if (j == SND_CHUNK_SIZE) { + chunk = chunk->next; + if (!chunk) { + chunk = sc->soundData; + } + samples = chunk->sndChunk; + ooff -= SND_CHUNK_SIZE; + } + if ( sc->soundChannels == 2 ) { + fdata[0] += samples[j&(SND_CHUNK_SIZE-1)]; + fdata[1] += samples[(j+1)&(SND_CHUNK_SIZE-1)]; + } else { + fdata[0] += samples[j&(SND_CHUNK_SIZE-1)]; + fdata[1] += samples[j&(SND_CHUNK_SIZE-1)]; + } + } + fdiv = 256 * (boff-aoff) / sc->soundChannels; + samp[i].left += (fdata[0] * fleftvol)/fdiv; + samp[i].right += (fdata[1] * frightvol)/fdiv; + } + } +} +#endif + +static void S_PaintChannelFrom16_scalar( channel_t *ch, const sfx_t *sc, int count, int sampleOffset, int bufferOffset ) { + int data, aoff, boff; + int leftvol, rightvol; + int i, j; + portable_samplepair_t *samp; + sndBuffer *chunk; + short *samples; + float ooff, fdata[2], fdiv, fleftvol, frightvol; + + if (sc->soundChannels <= 0) { + return; + } + + samp = &paintbuffer[ bufferOffset ]; + + if (ch->doppler) { + sampleOffset = sampleOffset*ch->oldDopplerScale; + } + + if ( sc->soundChannels == 2 ) { + sampleOffset *= sc->soundChannels; + + if ( sampleOffset & 1 ) { + sampleOffset &= ~1; + } + } + + chunk = sc->soundData; + while (sampleOffset>=SND_CHUNK_SIZE) { + chunk = chunk->next; + sampleOffset -= SND_CHUNK_SIZE; + if (!chunk) { + chunk = sc->soundData; + } + } + + if (!ch->doppler || ch->dopplerScale==1.0f) { + leftvol = ch->leftvol*snd_vol; + rightvol = ch->rightvol*snd_vol; + samples = chunk->sndChunk; + for ( i=0 ; i<count ; i++ ) { + data = samples[sampleOffset++]; + samp[i].left += (data * leftvol)>>8; + + if ( sc->soundChannels == 2 ) { + data = samples[sampleOffset++]; + } + samp[i].right += (data * rightvol)>>8; + + if (sampleOffset == SND_CHUNK_SIZE) { + chunk = chunk->next; + samples = chunk->sndChunk; + sampleOffset = 0; + } + } + } else { + fleftvol = ch->leftvol*snd_vol; + frightvol = ch->rightvol*snd_vol; + + ooff = sampleOffset; + samples = chunk->sndChunk; + + + + + for ( i=0 ; i<count ; i++ ) { + + aoff = ooff; + ooff = ooff + ch->dopplerScale * sc->soundChannels; + boff = ooff; + fdata[0] = fdata[1] = 0; + for (j=aoff; j<boff; j += sc->soundChannels) { + if (j == SND_CHUNK_SIZE) { + chunk = chunk->next; + if (!chunk) { + chunk = sc->soundData; + } + samples = chunk->sndChunk; + ooff -= SND_CHUNK_SIZE; + } + if ( sc->soundChannels == 2 ) { + fdata[0] += samples[j&(SND_CHUNK_SIZE-1)]; + fdata[1] += samples[(j+1)&(SND_CHUNK_SIZE-1)]; + } else { + fdata[0] += samples[j&(SND_CHUNK_SIZE-1)]; + fdata[1] += samples[j&(SND_CHUNK_SIZE-1)]; + } + } + fdiv = 256 * (boff-aoff) / sc->soundChannels; + samp[i].left += (fdata[0] * fleftvol)/fdiv; + samp[i].right += (fdata[1] * frightvol)/fdiv; + } + } +} + +static void S_PaintChannelFrom16( channel_t *ch, const sfx_t *sc, int count, int sampleOffset, int bufferOffset ) { +#if idppc_altivec + if (com_altivec->integer) { + // must be in a seperate function or G3 systems will crash. + S_PaintChannelFrom16_altivec( ch, sc, count, sampleOffset, bufferOffset ); + return; + } +#endif + S_PaintChannelFrom16_scalar( ch, sc, count, sampleOffset, bufferOffset ); +} + +void S_PaintChannelFromWavelet( channel_t *ch, sfx_t *sc, int count, int sampleOffset, int bufferOffset ) { + int data; + int leftvol, rightvol; + int i; + portable_samplepair_t *samp; + sndBuffer *chunk; + short *samples; + + leftvol = ch->leftvol*snd_vol; + rightvol = ch->rightvol*snd_vol; + + i = 0; + samp = &paintbuffer[ bufferOffset ]; + chunk = sc->soundData; + while (sampleOffset>=(SND_CHUNK_SIZE_FLOAT*4)) { + chunk = chunk->next; + sampleOffset -= (SND_CHUNK_SIZE_FLOAT*4); + i++; + } + + if (i!=sfxScratchIndex || sfxScratchPointer != sc) { + S_AdpcmGetSamples( chunk, sfxScratchBuffer ); + sfxScratchIndex = i; + sfxScratchPointer = sc; + } + + samples = sfxScratchBuffer; + + for ( i=0 ; i<count ; i++ ) { + data = samples[sampleOffset++]; + samp[i].left += (data * leftvol)>>8; + samp[i].right += (data * rightvol)>>8; + + if (sampleOffset == SND_CHUNK_SIZE*2) { + chunk = chunk->next; + decodeWavelet(chunk, sfxScratchBuffer); + sfxScratchIndex++; + sampleOffset = 0; + } + } +} + +void S_PaintChannelFromADPCM( channel_t *ch, sfx_t *sc, int count, int sampleOffset, int bufferOffset ) { + int data; + int leftvol, rightvol; + int i; + portable_samplepair_t *samp; + sndBuffer *chunk; + short *samples; + + leftvol = ch->leftvol*snd_vol; + rightvol = ch->rightvol*snd_vol; + + i = 0; + samp = &paintbuffer[ bufferOffset ]; + chunk = sc->soundData; + + if (ch->doppler) { + sampleOffset = sampleOffset*ch->oldDopplerScale; + } + + while (sampleOffset>=(SND_CHUNK_SIZE*4)) { + chunk = chunk->next; + sampleOffset -= (SND_CHUNK_SIZE*4); + i++; + } + + if (i!=sfxScratchIndex || sfxScratchPointer != sc) { + S_AdpcmGetSamples( chunk, sfxScratchBuffer ); + sfxScratchIndex = i; + sfxScratchPointer = sc; + } + + samples = sfxScratchBuffer; + + for ( i=0 ; i<count ; i++ ) { + data = samples[sampleOffset++]; + samp[i].left += (data * leftvol)>>8; + samp[i].right += (data * rightvol)>>8; + + if (sampleOffset == SND_CHUNK_SIZE*4) { + chunk = chunk->next; + S_AdpcmGetSamples( chunk, sfxScratchBuffer); + sampleOffset = 0; + sfxScratchIndex++; + } + } +} + +void S_PaintChannelFromMuLaw( channel_t *ch, sfx_t *sc, int count, int sampleOffset, int bufferOffset ) { + int data; + int leftvol, rightvol; + int i; + portable_samplepair_t *samp; + sndBuffer *chunk; + byte *samples; + float ooff; + + leftvol = ch->leftvol*snd_vol; + rightvol = ch->rightvol*snd_vol; + + samp = &paintbuffer[ bufferOffset ]; + chunk = sc->soundData; + while (sampleOffset>=(SND_CHUNK_SIZE*2)) { + chunk = chunk->next; + sampleOffset -= (SND_CHUNK_SIZE*2); + if (!chunk) { + chunk = sc->soundData; + } + } + + if (!ch->doppler) { + samples = (byte *)chunk->sndChunk + sampleOffset; + for ( i=0 ; i<count ; i++ ) { + data = mulawToShort[*samples]; + samp[i].left += (data * leftvol)>>8; + samp[i].right += (data * rightvol)>>8; + samples++; + if (chunk != NULL && samples == (byte *)chunk->sndChunk+(SND_CHUNK_SIZE*2)) { + chunk = chunk->next; + samples = (byte *)chunk->sndChunk; + } + } + } else { + ooff = sampleOffset; + samples = (byte *)chunk->sndChunk; + for ( i=0 ; i<count ; i++ ) { + data = mulawToShort[samples[(int)(ooff)]]; + ooff = ooff + ch->dopplerScale; + samp[i].left += (data * leftvol)>>8; + samp[i].right += (data * rightvol)>>8; + if (ooff >= SND_CHUNK_SIZE*2) { + chunk = chunk->next; + if (!chunk) { + chunk = sc->soundData; + } + samples = (byte *)chunk->sndChunk; + ooff = 0.0; + } + } + } +} + +/* +=================== +S_PaintChannels +=================== +*/ +void S_PaintChannels( int endtime ) { + int i; + int end; + int stream; + channel_t *ch; + sfx_t *sc; + int ltime, count; + int sampleOffset; + + if(s_muted->integer) + snd_vol = 0; + else + snd_vol = s_volume->value*255; + +//Com_Printf ("%i to %i\n", s_paintedtime, endtime); + while ( s_paintedtime < endtime ) { + // if paintbuffer is smaller than DMA buffer + // we may need to fill it multiple times + end = endtime; + if ( endtime - s_paintedtime > PAINTBUFFER_SIZE ) { + end = s_paintedtime + PAINTBUFFER_SIZE; + } + + // clear the paint buffer and mix any raw samples... + ::memset(paintbuffer, 0, sizeof (paintbuffer)); + for (stream = 0; stream < MAX_RAW_STREAMS; stream++) { + if ( s_rawend[stream] >= s_paintedtime ) { + // copy from the streaming sound source + const portable_samplepair_t *rawsamples = s_rawsamples[stream]; + const int stop = (end < s_rawend[stream]) ? end : s_rawend[stream]; + for ( i = s_paintedtime ; i < stop ; i++ ) { + const int s = i&(MAX_RAW_SAMPLES-1); + paintbuffer[i-s_paintedtime].left += rawsamples[s].left; + paintbuffer[i-s_paintedtime].right += rawsamples[s].right; + } + } + } + + // paint in the channels. + ch = s_channels; + for ( i = 0; i < MAX_CHANNELS ; i++, ch++ ) { + if ( !ch->thesfx || (ch->leftvol<0.25 && ch->rightvol<0.25 )) { + continue; + } + + ltime = s_paintedtime; + sc = ch->thesfx; + + if (sc->soundData==NULL || sc->soundLength==0) { + continue; + } + + sampleOffset = ltime - ch->startSample; + count = end - ltime; + if ( sampleOffset + count > sc->soundLength ) { + count = sc->soundLength - sampleOffset; + } + + if ( count > 0 ) { + if( sc->soundCompressionMethod == 1) { + S_PaintChannelFromADPCM (ch, sc, count, sampleOffset, ltime - s_paintedtime); + } else if( sc->soundCompressionMethod == 2) { + S_PaintChannelFromWavelet (ch, sc, count, sampleOffset, ltime - s_paintedtime); + } else if( sc->soundCompressionMethod == 3) { + S_PaintChannelFromMuLaw (ch, sc, count, sampleOffset, ltime - s_paintedtime); + } else { + S_PaintChannelFrom16 (ch, sc, count, sampleOffset, ltime - s_paintedtime); + } + } + } + + // paint in the looped channels. + ch = loop_channels; + for ( i = 0; i < numLoopChannels ; i++, ch++ ) { + if ( !ch->thesfx || (!ch->leftvol && !ch->rightvol )) { + continue; + } + + ltime = s_paintedtime; + sc = ch->thesfx; + + if (sc->soundData==NULL || sc->soundLength==0) { + continue; + } + // we might have to make two passes if it + // is a looping sound effect and the end of + // the sample is hit + do { + sampleOffset = (ltime % sc->soundLength); + + count = end - ltime; + if ( sampleOffset + count > sc->soundLength ) { + count = sc->soundLength - sampleOffset; + } + + if ( count > 0 ) { + if( sc->soundCompressionMethod == 1) { + S_PaintChannelFromADPCM (ch, sc, count, sampleOffset, ltime - s_paintedtime); + } else if( sc->soundCompressionMethod == 2) { + S_PaintChannelFromWavelet (ch, sc, count, sampleOffset, ltime - s_paintedtime); + } else if( sc->soundCompressionMethod == 3) { + S_PaintChannelFromMuLaw (ch, sc, count, sampleOffset, ltime - s_paintedtime); + } else { + S_PaintChannelFrom16 (ch, sc, count, sampleOffset, ltime - s_paintedtime); + } + ltime += count; + } + } while ( ltime < end); + } + + // transfer out according to DMA format + S_TransferPaintBuffer( end ); + s_paintedtime = end; + } +} diff --git a/src/client/snd_openal.cpp b/src/client/snd_openal.cpp new file mode 100644 index 0000000..7ee53e2 --- /dev/null +++ b/src/client/snd_openal.cpp @@ -0,0 +1,2737 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2005 Stuart Dalton (badcdev@gmail.com) +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#include "snd_local.h" +#include "snd_codec.h" +#include "client.h" + +#ifdef USE_OPENAL + +#include "qal.h" + +// Console variables specific to OpenAL +cvar_t *s_alPrecache; +cvar_t *s_alGain; +cvar_t *s_alSources; +cvar_t *s_alDopplerFactor; +cvar_t *s_alDopplerSpeed; +cvar_t *s_alMinDistance; +cvar_t *s_alMaxDistance; +cvar_t *s_alRolloff; +cvar_t *s_alGraceDistance; +cvar_t *s_alDriver; +cvar_t *s_alDevice; +cvar_t *s_alInputDevice; +cvar_t *s_alAvailableDevices; +cvar_t *s_alAvailableInputDevices; + +static bool enumeration_ext = false; +static bool enumeration_all_ext = false; +#ifdef USE_VOIP +static bool capture_ext = false; +#endif + +/* +================= +S_AL_Format +================= +*/ +static +ALuint S_AL_Format(int width, int channels) +{ + ALuint format = AL_FORMAT_MONO16; + + // Work out format + if(width == 1) + { + if(channels == 1) + format = AL_FORMAT_MONO8; + else if(channels == 2) + format = AL_FORMAT_STEREO8; + } + else if(width == 2) + { + if(channels == 1) + format = AL_FORMAT_MONO16; + else if(channels == 2) + format = AL_FORMAT_STEREO16; + } + + return format; +} + +/* +================= +S_AL_ErrorMsg +================= +*/ +static const char *S_AL_ErrorMsg(ALenum error) +{ + switch(error) + { + case AL_NO_ERROR: + return "No error"; + case AL_INVALID_NAME: + return "Invalid name"; + case AL_INVALID_ENUM: + return "Invalid enumerator"; + case AL_INVALID_VALUE: + return "Invalid value"; + case AL_INVALID_OPERATION: + return "Invalid operation"; + case AL_OUT_OF_MEMORY: + return "Out of memory"; + default: + return "Unknown error"; + } +} + +/* +================= +S_AL_ClearError +================= +*/ +static void S_AL_ClearError( bool quiet ) +{ + int error = qalGetError(); + + if( quiet ) + return; + if(error != AL_NO_ERROR) + { + Com_Printf(S_COLOR_YELLOW "WARNING: unhandled AL error: %s\n", + S_AL_ErrorMsg(error)); + } +} + + +//=========================================================================== + + +typedef struct alSfx_s +{ + char filename[MAX_QPATH]; + ALuint buffer; // OpenAL buffer + snd_info_t info; // information for this sound like rate, sample count.. + + bool isDefault; // Couldn't be loaded - use default FX + bool isDefaultChecked; // Sound has been check if it isDefault + bool inMemory; // Sound is stored in memory + bool isLocked; // Sound is locked (can not be unloaded) + int lastUsedTime; // Time last used + + int duration; // Milliseconds + + int loopCnt; // number of loops using this sfx + int loopActiveCnt; // number of playing loops using this sfx + int masterLoopSrc; // All other sources looping this buffer are synced to this master src +} alSfx_t; + +static bool alBuffersInitialised = false; + +// Sound effect storage, data structures +#define MAX_SFX 4096 +static alSfx_t knownSfx[MAX_SFX]; +static sfxHandle_t numSfx = 0; + +static sfxHandle_t default_sfx; + +/* +================= +S_AL_BufferFindFree + +Find a free handle +================= +*/ +static sfxHandle_t S_AL_BufferFindFree( void ) +{ + int i; + + for(i = 0; i < MAX_SFX; i++) + { + // Got one + if(knownSfx[i].filename[0] == '\0') + { + if(i >= numSfx) + numSfx = i + 1; + return i; + } + } + + // Shit... + Com_Error(ERR_FATAL, "S_AL_BufferFindFree: No free sound handles"); + return -1; +} + +/* +================= +S_AL_BufferFind + +Find a sound effect if loaded, set up a handle otherwise +================= +*/ +static sfxHandle_t S_AL_BufferFind(const char *filename) +{ + // Look it up in the table + sfxHandle_t sfx = -1; + int i; + + if ( !filename ) { + Com_Error( ERR_FATAL, "Sound name is NULL" ); + } + + if ( !filename[0] ) { + Com_Printf( S_COLOR_YELLOW "WARNING: Sound name is empty\n" ); + return 0; + } + + if ( strlen( filename ) >= MAX_QPATH ) { + Com_Printf( S_COLOR_YELLOW "WARNING: Sound name is too long: %s\n", filename ); + return 0; + } + + if ( filename[0] == '*' ) { + Com_Printf( S_COLOR_YELLOW "WARNING: Tried to load player sound directly: %s\n", filename ); + return 0; + } + + for(i = 0; i < numSfx; i++) + { + if(!Q_stricmp(knownSfx[i].filename, filename)) + { + sfx = i; + break; + } + } + + // Not found in table? + if(sfx == -1) + { + alSfx_t *ptr; + + sfx = S_AL_BufferFindFree(); + + // Clear and copy the filename over + ptr = &knownSfx[sfx]; + memset(ptr, 0, sizeof(*ptr)); + ptr->masterLoopSrc = -1; + strcpy(ptr->filename, filename); + } + + // Return the handle + return sfx; +} + +/* +================= +S_AL_BufferUseDefault +================= +*/ +static void S_AL_BufferUseDefault(sfxHandle_t sfx) +{ + if(sfx == default_sfx) + Com_Error(ERR_FATAL, "Can't load default sound effect %s", knownSfx[sfx].filename); + + Com_Printf( S_COLOR_YELLOW "WARNING: Using default sound for %s\n", knownSfx[sfx].filename); + knownSfx[sfx].isDefault = true; + knownSfx[sfx].buffer = knownSfx[default_sfx].buffer; +} + +/* +================= +S_AL_BufferUnload +================= +*/ +static void S_AL_BufferUnload(sfxHandle_t sfx) +{ + if(knownSfx[sfx].filename[0] == '\0') + return; + + if(!knownSfx[sfx].inMemory) + return; + + // Delete it + S_AL_ClearError( false ); + qalDeleteBuffers(1, &knownSfx[sfx].buffer); + if(qalGetError() != AL_NO_ERROR) + Com_Printf( S_COLOR_RED "ERROR: Can't delete sound buffer for %s\n", + knownSfx[sfx].filename); + + knownSfx[sfx].inMemory = false; +} + +/* +================= +S_AL_BufferEvict +================= +*/ +static bool S_AL_BufferEvict( void ) +{ + int i, oldestBuffer = -1; + int oldestTime = Sys_Milliseconds( ); + + for( i = 0; i < numSfx; i++ ) + { + if( !knownSfx[ i ].filename[ 0 ] ) + continue; + + if( !knownSfx[ i ].inMemory ) + continue; + + if( knownSfx[ i ].lastUsedTime < oldestTime ) + { + oldestTime = knownSfx[ i ].lastUsedTime; + oldestBuffer = i; + } + } + + if( oldestBuffer >= 0 ) + { + S_AL_BufferUnload( oldestBuffer ); + return true; + } + else + return false; +} + +/* +================= +S_AL_GenBuffers +================= +*/ +static bool S_AL_GenBuffers(ALsizei numBuffers, ALuint *buffers, const char *name) +{ + S_AL_ClearError( false ); + qalGenBuffers( numBuffers, buffers ); + ALenum error = qalGetError(); + + // If we ran out of buffers, start evicting the least recently used sounds + while( error == AL_INVALID_VALUE ) + { + if( !S_AL_BufferEvict( ) ) + { + Com_Printf( S_COLOR_RED "ERROR: Out of audio buffers\n"); + return false; + } + + // Try again + S_AL_ClearError( false ); + qalGenBuffers( numBuffers, buffers ); + error = qalGetError(); + } + + if( error != AL_NO_ERROR ) + { + Com_Printf( S_COLOR_RED "ERROR: Can't create a sound buffer for %s - %s\n", + name, S_AL_ErrorMsg(error)); + return false; + } + + return true; +} + +/* +================= +S_AL_BufferLoad +================= +*/ +static void S_AL_BufferLoad(sfxHandle_t sfx, bool cache) +{ + ALenum error; + ALuint format; + + void *data; + snd_info_t info; + alSfx_t *curSfx = &knownSfx[sfx]; + + // Nothing? + if(curSfx->filename[0] == '\0') + return; + + // Already done? + if((curSfx->inMemory) || (curSfx->isDefault) || (!cache && curSfx->isDefaultChecked)) + return; + + // Try to load + data = S_CodecLoad(curSfx->filename, &info); + if(!data) + { + S_AL_BufferUseDefault(sfx); + return; + } + + curSfx->isDefaultChecked = true; + + if (!cache) + { + // Don't create AL cache + Hunk_FreeTempMemory(data); + return; + } + + format = S_AL_Format(info.width, info.channels); + + // Create a buffer + if (!S_AL_GenBuffers(1, &curSfx->buffer, curSfx->filename)) + { + S_AL_BufferUseDefault(sfx); + Hunk_FreeTempMemory(data); + return; + } + + // Fill the buffer + if( info.size == 0 ) + { + // We have no data to buffer, so buffer silence + byte dummyData[ 2 ] = { 0 }; + + qalBufferData(curSfx->buffer, AL_FORMAT_MONO16, (void *)dummyData, 2, 22050); + } + else + qalBufferData(curSfx->buffer, format, data, info.size, info.rate); + + error = qalGetError(); + + // If we ran out of memory, start evicting the least recently used sounds + while(error == AL_OUT_OF_MEMORY) + { + if( !S_AL_BufferEvict( ) ) + { + qalDeleteBuffers(1, &curSfx->buffer); + S_AL_BufferUseDefault(sfx); + Hunk_FreeTempMemory(data); + Com_Printf( S_COLOR_RED "ERROR: Out of memory loading %s\n", curSfx->filename); + return; + } + + // Try load it again + qalBufferData(curSfx->buffer, format, data, info.size, info.rate); + error = qalGetError(); + } + + // Some other error condition + if(error != AL_NO_ERROR) + { + qalDeleteBuffers(1, &curSfx->buffer); + S_AL_BufferUseDefault(sfx); + Hunk_FreeTempMemory(data); + Com_Printf( S_COLOR_RED "ERROR: Can't fill sound buffer for %s - %s\n", + curSfx->filename, S_AL_ErrorMsg(error)); + return; + } + + curSfx->info = info; + + // Free the memory + Hunk_FreeTempMemory(data); + + // Woo! + curSfx->inMemory = true; +} + +/* +================= +S_AL_BufferUse +================= +*/ +static void S_AL_BufferUse(sfxHandle_t sfx) +{ + if(knownSfx[sfx].filename[0] == '\0') + return; + + if((!knownSfx[sfx].inMemory) && (!knownSfx[sfx].isDefault)) + S_AL_BufferLoad(sfx, true); + knownSfx[sfx].lastUsedTime = Sys_Milliseconds(); +} + +/* +================= +S_AL_BufferInit +================= +*/ +static bool S_AL_BufferInit( void ) +{ + if(alBuffersInitialised) + return true; + + // Clear the hash table, and SFX table + memset(knownSfx, 0, sizeof(knownSfx)); + numSfx = 0; + + // Load the default sound, and lock it + default_sfx = S_AL_BufferFind("sound/feedback/hit.wav"); + S_AL_BufferUse(default_sfx); + knownSfx[default_sfx].isLocked = true; + + // All done + alBuffersInitialised = true; + return true; +} + +/* +================= +S_AL_BufferShutdown +================= +*/ +static void S_AL_BufferShutdown( void ) +{ + if(!alBuffersInitialised) + return; + + // Unlock the default sound effect + knownSfx[default_sfx].isLocked = false; + + // Free all used effects + for(int i = 0; i < numSfx; i++) + S_AL_BufferUnload(i); + + // Clear the tables + numSfx = 0; + + // All undone + alBuffersInitialised = false; +} + +/* +================= +S_AL_RegisterSound +================= +*/ +static sfxHandle_t S_AL_RegisterSound( const char *sample, bool compressed ) +{ + sfxHandle_t sfx = S_AL_BufferFind(sample); + + if((!knownSfx[sfx].inMemory) && (!knownSfx[sfx].isDefault)) + S_AL_BufferLoad(sfx, s_alPrecache->integer ? true : false); + knownSfx[sfx].lastUsedTime = Com_Milliseconds(); + + if (knownSfx[sfx].isDefault) { + return 0; + } + + return sfx; +} + +/* +================= +S_AL_SoundDuration +================= +*/ +static int S_AL_SoundDuration( sfxHandle_t sfx ) +{ + if (sfx < 0 || sfx >= numSfx) + { + Com_Printf(S_COLOR_RED "ERROR: S_AL_SoundDuration: handle %i out of range\n", sfx); + return 0; + } + return knownSfx[sfx].duration; +} + +/* +================= +S_AL_BufferGet + +Return's a sfx's buffer +================= +*/ +static +ALuint S_AL_BufferGet(sfxHandle_t sfx) +{ + return knownSfx[sfx].buffer; +} + + +//=========================================================================== + + +typedef struct src_s +{ + ALuint alSource; // OpenAL source object + sfxHandle_t sfx; // Sound effect in use + + int lastUsedTime; // Last time used + alSrcPriority_t priority; // Priority + int entity; // Owning entity (-1 if none) + int channel; // Associated channel (-1 if none) + + bool isActive; // Is this source currently in use? + bool isPlaying; // Is this source currently playing, or stopped? + bool isLocked; // This is locked (un-allocatable) + bool isLooping; // Is this a looping effect (attached to an entity) + bool isTracking; // Is this object tracking its owner + bool isStream; // Is this source a stream + + float curGain; // gain employed if source is within maxdistance. + float scaleGain; // Last gain value for this source. 0 if muted. + + float lastTimePos; // On stopped loops, the last position in the buffer + int lastSampleTime; // Time when this was stopped + vec3_t loopSpeakerPos; // Origin of the loop speaker + + bool local; // Is this local (relative to the cam) +} src_t; + +#ifdef __APPLE__ + #define MAX_SRC 64 +#else + #define MAX_SRC 128 +#endif +static src_t srcList[MAX_SRC]; +static int srcCount = 0; +static int srcActiveCnt = 0; +static bool alSourcesInitialised = false; +static int lastListenerNumber = -1; +static vec3_t lastListenerOrigin = { 0.0f, 0.0f, 0.0f }; + +typedef struct sentity_s +{ + vec3_t origin; + + bool srcAllocated; // If a src_t has been allocated to this entity + int srcIndex; + + bool loopAddedThisFrame; + alSrcPriority_t loopPriority; + sfxHandle_t loopSfx; + bool startLoopingSound; +} sentity_t; + +static sentity_t entityList[MAX_GENTITIES]; + +/* +================= +S_AL_SanitiseVector +================= +*/ +#define S_AL_SanitiseVector(v) _S_AL_SanitiseVector(v,__LINE__) +static void _S_AL_SanitiseVector( vec3_t v, int line ) +{ + if( Q_isnan( v[ 0 ] ) || Q_isnan( v[ 1 ] ) || Q_isnan( v[ 2 ] ) ) + { + Com_DPrintf( S_COLOR_YELLOW "WARNING: vector with one or more NaN components " + "being passed to OpenAL at %s:%d -- zeroing\n", __FILE__, line ); + VectorClear( v ); + } +} + +/* +================= +S_AL_Gain +Set gain to 0 if muted, otherwise set it to given value. +================= +*/ + +static void S_AL_Gain(ALuint source, float gainval) +{ + if(s_muted->integer) + qalSourcef(source, AL_GAIN, 0.0f); + else + qalSourcef(source, AL_GAIN, gainval); +} + +/* +================= +S_AL_ScaleGain +Adapt the gain if necessary to get a quicker fadeout when the source is too far away. +================= +*/ + +static void S_AL_ScaleGain(src_t *chksrc, vec3_t origin) +{ + float distance; + + if(!chksrc->local) + distance = Distance(origin, lastListenerOrigin); + + // If we exceed a certain distance, scale the gain linearly until the sound + // vanishes into nothingness. + if(!chksrc->local && (distance -= s_alMaxDistance->value) > 0) + { + float scaleFactor; + + if(distance >= s_alGraceDistance->value) + scaleFactor = 0.0f; + else + scaleFactor = 1.0f - distance / s_alGraceDistance->value; + + scaleFactor *= chksrc->curGain; + + if(chksrc->scaleGain != scaleFactor) + { + chksrc->scaleGain = scaleFactor; + S_AL_Gain(chksrc->alSource, chksrc->scaleGain); + } + } + else if(chksrc->scaleGain != chksrc->curGain) + { + chksrc->scaleGain = chksrc->curGain; + S_AL_Gain(chksrc->alSource, chksrc->scaleGain); + } +} + +/* +================= +S_AL_HearingThroughEntity + +Also see S_Base_HearingThroughEntity +================= +*/ +static bool S_AL_HearingThroughEntity( int entityNum ) +{ + + if( lastListenerNumber == entityNum ) + { + // This is an outrageous hack to detect + // whether or not the player is rendering in third person or not. We can't + // ask the renderer because the renderer has no notion of entities and we + // can't ask cgame since that would involve changing the API and hence mod + // compatibility. I don't think there is any way around this, but I'll leave + // the FIXME just in case anyone has a bright idea. + float distanceSq = DistanceSquared( entityList[ entityNum ].origin, lastListenerOrigin ); + if( distanceSq > THIRD_PERSON_THRESHOLD_SQ ) + return false; //we're the player, but third person + + //we're the player + return true; + } + + //not the player + return false; +} + +/* +================= +S_AL_SrcInit +================= +*/ +static bool S_AL_SrcInit( void ) +{ + int i; + int limit; + + // Clear the sources data structure + memset(srcList, 0, sizeof(srcList)); + srcCount = 0; + srcActiveCnt = 0; + + // Cap s_alSources to MAX_SRC + limit = s_alSources->integer; + if(limit > MAX_SRC) + limit = MAX_SRC; + else if(limit < 16) + limit = 16; + + S_AL_ClearError( false ); + // Allocate as many sources as possible + for(i = 0; i < limit; i++) + { + qalGenSources(1, &srcList[i].alSource); + if(qalGetError() != AL_NO_ERROR) + break; + srcCount++; + } + + // All done. Print this for informational purposes + Com_Printf( "Allocated %d sources.\n", srcCount); + alSourcesInitialised = true; + return true; +} + +/* +================= +S_AL_SrcShutdown +================= +*/ +static +void S_AL_SrcShutdown( void ) +{ + int i; + src_t *curSource; + + if(!alSourcesInitialised) + return; + + // Destroy all the sources + for(i = 0; i < srcCount; i++) + { + curSource = &srcList[i]; + + if(curSource->isLocked) + Com_DPrintf( S_COLOR_YELLOW "WARNING: Source %d is locked\n", i); + + if(curSource->entity > 0) + entityList[curSource->entity].srcAllocated = false; + + qalSourceStop(srcList[i].alSource); + qalDeleteSources(1, &srcList[i].alSource); + } + + memset(srcList, 0, sizeof(srcList)); + + alSourcesInitialised = false; +} + +/* +================= +S_AL_SrcSetup +================= +*/ +static void S_AL_SrcSetup(srcHandle_t src, sfxHandle_t sfx, alSrcPriority_t priority, int entity, int channel, bool local) +{ + src_t *curSource; + + // Set up src struct + curSource = &srcList[src]; + + curSource->lastUsedTime = Sys_Milliseconds(); + curSource->sfx = sfx; + curSource->priority = priority; + curSource->entity = entity; + curSource->channel = channel; + curSource->isPlaying = false; + curSource->isLocked = false; + curSource->isLooping = false; + curSource->isTracking = false; + curSource->isStream = false; + curSource->curGain = s_alGain->value * s_volume->value; + curSource->scaleGain = curSource->curGain; + curSource->local = local; + + // Set up OpenAL source + if(sfx >= 0) + { + // Mark the SFX as used, and grab the raw AL buffer + S_AL_BufferUse(sfx); + qalSourcei(curSource->alSource, AL_BUFFER, S_AL_BufferGet(sfx)); + } + + qalSourcef(curSource->alSource, AL_PITCH, 1.0f); + S_AL_Gain(curSource->alSource, curSource->curGain); + qalSourcefv(curSource->alSource, AL_POSITION, vec3_origin); + qalSourcefv(curSource->alSource, AL_VELOCITY, vec3_origin); + qalSourcei(curSource->alSource, AL_LOOPING, AL_FALSE); + qalSourcef(curSource->alSource, AL_REFERENCE_DISTANCE, s_alMinDistance->value); + + if(local) + { + qalSourcei(curSource->alSource, AL_SOURCE_RELATIVE, AL_TRUE); + qalSourcef(curSource->alSource, AL_ROLLOFF_FACTOR, 0.0f); + } + else + { + qalSourcei(curSource->alSource, AL_SOURCE_RELATIVE, AL_FALSE); + qalSourcef(curSource->alSource, AL_ROLLOFF_FACTOR, s_alRolloff->value); + } +} + +/* +================= +S_AL_SaveLoopPos +Remove given source as loop master if it is the master and hand off master status to another source in this case. +================= +*/ + +static void S_AL_SaveLoopPos(src_t *dest, ALuint alSource) +{ + int error; + + S_AL_ClearError(false); + + qalGetSourcef(alSource, AL_SEC_OFFSET, &dest->lastTimePos); + if((error = qalGetError()) != AL_NO_ERROR) + { + // Old OpenAL implementations don't support AL_SEC_OFFSET + + if(error != AL_INVALID_ENUM) + { + Com_Printf(S_COLOR_YELLOW "WARNING: Could not get time offset for alSource %d: %s\n", + alSource, S_AL_ErrorMsg(error)); + } + + dest->lastTimePos = -1; + } + else + dest->lastSampleTime = Sys_Milliseconds(); +} + +/* +================= +S_AL_NewLoopMaster +Remove given source as loop master if it is the master and hand off master status to another source in this case. +================= +*/ + +static void S_AL_NewLoopMaster(src_t *rmSource, bool iskilled) +{ + int index; + src_t *curSource = NULL; + alSfx_t *curSfx; + + curSfx = &knownSfx[rmSource->sfx]; + + if(rmSource->isPlaying) + curSfx->loopActiveCnt--; + if(iskilled) + curSfx->loopCnt--; + + if(curSfx->loopCnt) + { + if(rmSource->priority == SRCPRI_ENTITY) + { + if(!iskilled && rmSource->isPlaying) + { + // only sync ambient loops... + // It makes more sense to have sounds for weapons/projectiles unsynced + S_AL_SaveLoopPos(rmSource, rmSource->alSource); + } + } + else if(rmSource == &srcList[curSfx->masterLoopSrc]) + { + int firstInactive = -1; + + // Only if rmSource was the master and if there are still playing loops for + // this sound will we need to find a new master. + + if(iskilled || curSfx->loopActiveCnt) + { + for(index = 0; index < srcCount; index++) + { + curSource = &srcList[index]; + + if(curSource->sfx == rmSource->sfx && curSource != rmSource && + curSource->isActive && curSource->isLooping && curSource->priority == SRCPRI_AMBIENT) + { + if(curSource->isPlaying) + { + curSfx->masterLoopSrc = index; + break; + } + else if(firstInactive < 0) + firstInactive = index; + } + } + } + + if(!curSfx->loopActiveCnt) + { + if(firstInactive < 0) + { + if(iskilled) + { + curSfx->masterLoopSrc = -1; + return; + } + else + curSource = rmSource; + } + else + curSource = &srcList[firstInactive]; + + if(rmSource->isPlaying) + { + // this was the last not stopped source, save last sample position + time + S_AL_SaveLoopPos(curSource, rmSource->alSource); + } + else + { + // second case: all loops using this sound have stopped due to listener being of of range, + // and now the inactive master gets deleted. Just move over the soundpos settings to the + // new master. + curSource->lastTimePos = rmSource->lastTimePos; + curSource->lastSampleTime = rmSource->lastSampleTime; + } + } + } + } + else + curSfx->masterLoopSrc = -1; +} + +/* +================= +S_AL_SrcKill +================= +*/ +static void S_AL_SrcKill(srcHandle_t src) +{ + src_t *curSource = &srcList[src]; + + // I'm not touching it. Unlock it first. + if(curSource->isLocked) + return; + + // Remove the entity association and loop master status + if(curSource->isLooping) + { + curSource->isLooping = false; + + if(curSource->entity != -1) + { + sentity_t *curEnt = &entityList[curSource->entity]; + + curEnt->srcAllocated = false; + curEnt->srcIndex = -1; + curEnt->loopAddedThisFrame = false; + curEnt->startLoopingSound = false; + } + + S_AL_NewLoopMaster(curSource, true); + } + + // Stop it if it's playing + if(curSource->isPlaying) + { + qalSourceStop(curSource->alSource); + curSource->isPlaying = false; + } + + // Detach any buffers + qalSourcei(curSource->alSource, AL_BUFFER, 0); + + curSource->sfx = 0; + curSource->lastUsedTime = 0; + curSource->priority = SRCPRI_AMBIENT; + curSource->entity = -1; + curSource->channel = -1; + if(curSource->isActive) + { + curSource->isActive = false; + srcActiveCnt--; + } + curSource->isLocked = false; + curSource->isTracking = false; + curSource->local = false; +} + +/* +================= +S_AL_SrcAlloc +================= +*/ +static +srcHandle_t S_AL_SrcAlloc( alSrcPriority_t priority, int entnum, int channel ) +{ + int i; + int empty = -1; + int weakest = -1; + int weakest_time = Sys_Milliseconds(); + int weakest_pri = 999; + float weakest_gain = 1000.0; + bool weakest_isplaying = true; + int weakest_numloops = 0; + src_t *curSource; + + for(i = 0; i < srcCount; i++) + { + curSource = &srcList[i]; + + // If it's locked, we aren't even going to look at it + if(curSource->isLocked) + continue; + + // Is it empty or not? + if(!curSource->isActive) + { + empty = i; + break; + } + + if(curSource->isPlaying) + { + if(weakest_isplaying && curSource->priority < priority && + (curSource->priority < weakest_pri || + (!curSource->isLooping && (curSource->scaleGain < weakest_gain || curSource->lastUsedTime < weakest_time)))) + { + // If it has lower priority, is fainter or older, flag it as weak + // the last two values are only compared if it's not a looping sound, because we want to prevent two + // loops (loops are added EVERY frame) fighting for a slot + weakest_pri = curSource->priority; + weakest_time = curSource->lastUsedTime; + weakest_gain = curSource->scaleGain; + weakest = i; + } + } + else + { + weakest_isplaying = false; + + if(weakest < 0 || + knownSfx[curSource->sfx].loopCnt > weakest_numloops || + curSource->priority < weakest_pri || + curSource->lastUsedTime < weakest_time) + { + // Sources currently not playing of course have lowest priority + // also try to always keep at least one loop master for every loop sound + weakest_pri = curSource->priority; + weakest_time = curSource->lastUsedTime; + weakest_numloops = knownSfx[curSource->sfx].loopCnt; + weakest = i; + } + } + + // The channel system is not actually adhered to by baseq3, and not + // implemented in snd_dma.c, so while the following is strictly correct, it + // causes incorrect behaviour versus defacto baseq3 +#if 0 + // Is it an exact match, and not on channel 0? + if((curSource->entity == entnum) && (curSource->channel == channel) && (channel != 0)) + { + S_AL_SrcKill(i); + return i; + } +#endif + } + + if(empty == -1) + empty = weakest; + + if(empty >= 0) + { + S_AL_SrcKill(empty); + srcList[empty].isActive = true; + srcActiveCnt++; + } + + return empty; +} + +/* +================= +S_AL_SrcFind + +Finds an active source with matching entity and channel numbers +Returns -1 if there isn't one +================= +*/ +#if 0 +static +srcHandle_t S_AL_SrcFind(int entnum, int channel) +{ + int i; + for(i = 0; i < srcCount; i++) + { + if(!srcList[i].isActive) + continue; + if((srcList[i].entity == entnum) && (srcList[i].channel == channel)) + return i; + } + return -1; +} +#endif + +/* +================= +S_AL_SrcLock + +Locked sources will not be automatically reallocated or managed +================= +*/ +static +void S_AL_SrcLock(srcHandle_t src) +{ + srcList[src].isLocked = true; +} + +/* +================= +S_AL_SrcUnlock + +Once unlocked, the source may be reallocated again +================= +*/ +static +void S_AL_SrcUnlock(srcHandle_t src) +{ + srcList[src].isLocked = false; +} + +/* +================= +S_AL_UpdateEntityPosition +================= +*/ +static +void S_AL_UpdateEntityPosition( int entityNum, const vec3_t origin ) +{ + vec3_t sanOrigin; + + VectorCopy( origin, sanOrigin ); + S_AL_SanitiseVector( sanOrigin ); + if ( entityNum < 0 || entityNum >= MAX_GENTITIES ) + Com_Error( ERR_DROP, "S_UpdateEntityPosition: bad entitynum %i", entityNum ); + VectorCopy( sanOrigin, entityList[entityNum].origin ); +} + +/* +================= +S_AL_CheckInput +Check whether input values from mods are out of range. +Necessary for i.g. Western Quake3 mod which is buggy. +================= +*/ +static bool S_AL_CheckInput(int entityNum, sfxHandle_t sfx) +{ + if (entityNum < 0 || entityNum >= MAX_GENTITIES) + Com_Error(ERR_DROP, "ERROR: S_AL_CheckInput: bad entitynum %i", entityNum); + + if (sfx < 0 || sfx >= numSfx) + { + Com_Printf(S_COLOR_RED "ERROR: S_AL_CheckInput: handle %i out of range\n", sfx); + return true; + } + + return false; +} + +/* +================= +S_AL_StartLocalSound + +Play a local (non-spatialized) sound effect +================= +*/ +static +void S_AL_StartLocalSound(sfxHandle_t sfx, int channel) +{ + srcHandle_t src; + + if(S_AL_CheckInput(0, sfx)) + return; + + // Try to grab a source + src = S_AL_SrcAlloc(SRCPRI_LOCAL, -1, channel); + + if(src == -1) + return; + + // Set up the effect + S_AL_SrcSetup(src, sfx, SRCPRI_LOCAL, -1, channel, true); + + // Start it playing + srcList[src].isPlaying = true; + qalSourcePlay(srcList[src].alSource); +} + +/* +================= +S_AL_StartSound + +Play a one-shot sound effect +================= +*/ +static void S_AL_StartSound( vec3_t origin, int entnum, int entchannel, sfxHandle_t sfx ) +{ + vec3_t sorigin; + srcHandle_t src; + src_t *curSource; + + if(origin) + { + if(S_AL_CheckInput(0, sfx)) + return; + + VectorCopy(origin, sorigin); + } + else + { + if(S_AL_CheckInput(entnum, sfx)) + return; + + if(S_AL_HearingThroughEntity(entnum)) + { + S_AL_StartLocalSound(sfx, entchannel); + return; + } + + VectorCopy(entityList[entnum].origin, sorigin); + } + + S_AL_SanitiseVector(sorigin); + + if((srcActiveCnt > 5 * srcCount / 3) && + (DistanceSquared(sorigin, lastListenerOrigin) >= + (s_alMaxDistance->value + s_alGraceDistance->value) * (s_alMaxDistance->value + s_alGraceDistance->value))) + { + // We're getting tight on sources and source is not within hearing distance so don't add it + return; + } + + // Try to grab a source + src = S_AL_SrcAlloc(SRCPRI_ONESHOT, entnum, entchannel); + if(src == -1) + return; + + S_AL_SrcSetup(src, sfx, SRCPRI_ONESHOT, entnum, entchannel, false); + + curSource = &srcList[src]; + + if(!origin) + curSource->isTracking = true; + + qalSourcefv(curSource->alSource, AL_POSITION, sorigin ); + S_AL_ScaleGain(curSource, sorigin); + + // Start it playing + curSource->isPlaying = true; + qalSourcePlay(curSource->alSource); +} + +/* +================= +S_AL_ClearLoopingSounds +================= +*/ +static void S_AL_ClearLoopingSounds( bool killall ) +{ + int i; + for(i = 0; i < srcCount; i++) + { + if((srcList[i].isLooping) && (srcList[i].entity != -1)) + entityList[srcList[i].entity].loopAddedThisFrame = false; + } +} + +/* +================= +S_AL_SrcLoop +================= +*/ +static void S_AL_SrcLoop( alSrcPriority_t priority, sfxHandle_t sfx, + const vec3_t origin, const vec3_t velocity, int entityNum ) +{ + int src; + sentity_t *sent = &entityList[ entityNum ]; + src_t *curSource; + vec3_t sorigin, svelocity; + + if(S_AL_CheckInput(entityNum, sfx)) + return; + + // Do we need to allocate a new source for this entity + if( !sent->srcAllocated ) + { + // Try to get a channel + src = S_AL_SrcAlloc( priority, entityNum, -1 ); + if( src == -1 ) + { + Com_DPrintf( S_COLOR_YELLOW "WARNING: Failed to allocate source " + "for loop sfx %d on entity %d\n", sfx, entityNum ); + return; + } + + curSource = &srcList[src]; + + sent->startLoopingSound = true; + + curSource->lastTimePos = -1.0; + curSource->lastSampleTime = Sys_Milliseconds(); + } + else + { + src = sent->srcIndex; + curSource = &srcList[src]; + } + + sent->srcAllocated = true; + sent->srcIndex = src; + + sent->loopPriority = priority; + sent->loopSfx = sfx; + + // If this is not set then the looping sound is stopped. + sent->loopAddedThisFrame = true; + + // UGH + // These lines should be called via S_AL_SrcSetup, but we + // can't call that yet as it buffers sfxes that may change + // with subsequent calls to S_AL_SrcLoop + curSource->entity = entityNum; + curSource->isLooping = true; + + if( S_AL_HearingThroughEntity( entityNum ) ) + { + curSource->local = true; + + VectorClear(sorigin); + + qalSourcefv(curSource->alSource, AL_POSITION, sorigin); + qalSourcefv(curSource->alSource, AL_VELOCITY, vec3_origin); + } + else + { + curSource->local = false; + + if(origin) + VectorCopy(origin, sorigin); + else + VectorCopy(sent->origin, sorigin); + + S_AL_SanitiseVector(sorigin); + + VectorCopy(sorigin, curSource->loopSpeakerPos); + + if(velocity) + { + VectorCopy(velocity, svelocity); + S_AL_SanitiseVector(svelocity); + } + else + VectorClear(svelocity); + + qalSourcefv(curSource->alSource, AL_POSITION, (ALfloat *) sorigin); + qalSourcefv(curSource->alSource, AL_VELOCITY, (ALfloat *) svelocity); + } +} + +/* +================= +S_AL_AddLoopingSound +================= +*/ +static void S_AL_AddLoopingSound(int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfx) +{ + S_AL_SrcLoop(SRCPRI_ENTITY, sfx, origin, velocity, entityNum); +} + +/* +================= +S_AL_AddRealLoopingSound +================= +*/ +static void S_AL_AddRealLoopingSound(int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfx) +{ + S_AL_SrcLoop(SRCPRI_AMBIENT, sfx, origin, velocity, entityNum); +} + +/* +================= +S_AL_StopLoopingSound +================= +*/ +static +void S_AL_StopLoopingSound(int entityNum ) +{ + if(entityList[entityNum].srcAllocated) + S_AL_SrcKill(entityList[entityNum].srcIndex); +} + +/* +================= +S_AL_SrcUpdate + +Update state (move things around, manage sources, and so on) +================= +*/ +static +void S_AL_SrcUpdate( void ) +{ + int i; + int entityNum; + ALint state; + src_t *curSource; + + for(i = 0; i < srcCount; i++) + { + entityNum = srcList[i].entity; + curSource = &srcList[i]; + + if(curSource->isLocked) + continue; + + if(!curSource->isActive) + continue; + + // Update source parameters + if((s_alGain->modified) || (s_volume->modified)) + curSource->curGain = s_alGain->value * s_volume->value; + if((s_alRolloff->modified) && (!curSource->local)) + qalSourcef(curSource->alSource, AL_ROLLOFF_FACTOR, s_alRolloff->value); + if(s_alMinDistance->modified) + qalSourcef(curSource->alSource, AL_REFERENCE_DISTANCE, s_alMinDistance->value); + + if(curSource->isLooping) + { + sentity_t *sent = &entityList[ entityNum ]; + + // If a looping effect hasn't been touched this frame, pause or kill it + if(sent->loopAddedThisFrame) + { + alSfx_t *curSfx; + + // The sound has changed without an intervening removal + if(curSource->isActive && !sent->startLoopingSound && + curSource->sfx != sent->loopSfx) + { + S_AL_NewLoopMaster(curSource, true); + + curSource->isPlaying = false; + qalSourceStop(curSource->alSource); + qalSourcei(curSource->alSource, AL_BUFFER, 0); + sent->startLoopingSound = true; + } + + // The sound hasn't been started yet + if(sent->startLoopingSound) + { + S_AL_SrcSetup(i, sent->loopSfx, sent->loopPriority, + entityNum, -1, curSource->local); + curSource->isLooping = true; + + knownSfx[curSource->sfx].loopCnt++; + sent->startLoopingSound = false; + } + + curSfx = &knownSfx[curSource->sfx]; + + S_AL_ScaleGain(curSource, curSource->loopSpeakerPos); + if(!curSource->scaleGain) + { + if(curSource->isPlaying) + { + // Sound is mute, stop playback until we are in range again + S_AL_NewLoopMaster(curSource, false); + qalSourceStop(curSource->alSource); + curSource->isPlaying = false; + } + else if(!curSfx->loopActiveCnt && curSfx->masterLoopSrc < 0) + curSfx->masterLoopSrc = i; + + continue; + } + + if(!curSource->isPlaying) + { + qalSourcei(curSource->alSource, AL_LOOPING, AL_TRUE); + curSource->isPlaying = true; + qalSourcePlay(curSource->alSource); + + if(curSource->priority == SRCPRI_AMBIENT) + { + // If there are other ambient looping sources with the same sound, + // make sure the sound of these sources are in sync. + + if(curSfx->loopActiveCnt) + { + int offset, error; + + // we already have a master loop playing, get buffer position. + S_AL_ClearError(false); + qalGetSourcei(srcList[curSfx->masterLoopSrc].alSource, AL_SAMPLE_OFFSET, &offset); + if((error = qalGetError()) != AL_NO_ERROR) + { + if(error != AL_INVALID_ENUM) + { + Com_Printf(S_COLOR_YELLOW "WARNING: Cannot get sample offset from source %d: " + "%s\n", i, S_AL_ErrorMsg(error)); + } + } + else + qalSourcei(curSource->alSource, AL_SAMPLE_OFFSET, offset); + } + else if(curSfx->loopCnt && curSfx->masterLoopSrc >= 0) + { + float secofs; + + src_t *master = &srcList[curSfx->masterLoopSrc]; + // This loop sound used to be played, but all sources are stopped. Use last sample position/time + // to calculate offset so the player thinks the sources continued playing while they were inaudible. + + if(master->lastTimePos >= 0) + { + secofs = master->lastTimePos + (Sys_Milliseconds() - master->lastSampleTime) / 1000.0f; + secofs = fmodf(secofs, (float) curSfx->info.samples / curSfx->info.rate); + + qalSourcef(curSource->alSource, AL_SEC_OFFSET, secofs); + } + + // I be the master now + curSfx->masterLoopSrc = i; + } + else + curSfx->masterLoopSrc = i; + } + else if(curSource->lastTimePos >= 0) + { + float secofs; + + // For unsynced loops (SRCPRI_ENTITY) just carry on playing as if the sound was never stopped + + secofs = curSource->lastTimePos + (Sys_Milliseconds() - curSource->lastSampleTime) / 1000.0f; + secofs = fmodf(secofs, (float) curSfx->info.samples / curSfx->info.rate); + qalSourcef(curSource->alSource, AL_SEC_OFFSET, secofs); + } + + curSfx->loopActiveCnt++; + } + + // Update locality + if(curSource->local) + { + qalSourcei(curSource->alSource, AL_SOURCE_RELATIVE, AL_TRUE); + qalSourcef(curSource->alSource, AL_ROLLOFF_FACTOR, 0.0f); + } + else + { + qalSourcei(curSource->alSource, AL_SOURCE_RELATIVE, AL_FALSE); + qalSourcef(curSource->alSource, AL_ROLLOFF_FACTOR, s_alRolloff->value); + } + + } + else if(curSource->priority == SRCPRI_AMBIENT) + { + if(curSource->isPlaying) + { + S_AL_NewLoopMaster(curSource, false); + qalSourceStop(curSource->alSource); + curSource->isPlaying = false; + } + } + else + S_AL_SrcKill(i); + + continue; + } + + if(!curSource->isStream) + { + // Check if it's done, and flag it + qalGetSourcei(curSource->alSource, AL_SOURCE_STATE, &state); + if(state == AL_STOPPED) + { + curSource->isPlaying = false; + S_AL_SrcKill(i); + continue; + } + } + + // Query relativity of source, don't move if it's true + qalGetSourcei(curSource->alSource, AL_SOURCE_RELATIVE, &state); + + // See if it needs to be moved + if(curSource->isTracking && !state) + { + qalSourcefv(curSource->alSource, AL_POSITION, entityList[entityNum].origin); + S_AL_ScaleGain(curSource, entityList[entityNum].origin); + } + } +} + +/* +================= +S_AL_SrcShutup +================= +*/ +static +void S_AL_SrcShutup( void ) +{ + int i; + for(i = 0; i < srcCount; i++) + S_AL_SrcKill(i); +} + +/* +================= +S_AL_SrcGet +================= +*/ +static +ALuint S_AL_SrcGet(srcHandle_t src) +{ + return srcList[src].alSource; +} + + +//=========================================================================== + +// Q3A cinematics use up to 12 buffers at once +#define MAX_STREAM_BUFFERS 20 + +static srcHandle_t streamSourceHandles[MAX_RAW_STREAMS]; +static bool streamPlaying[MAX_RAW_STREAMS]; +static ALuint streamSources[MAX_RAW_STREAMS]; +static ALuint streamBuffers[MAX_RAW_STREAMS][MAX_STREAM_BUFFERS]; +static int streamNumBuffers[MAX_RAW_STREAMS]; +static int streamBufIndex[MAX_RAW_STREAMS]; + +/* +================= +S_AL_AllocateStreamChannel +================= +*/ +static void S_AL_AllocateStreamChannel(int stream, int entityNum) +{ + srcHandle_t cursrc; + ALuint alsrc; + + if ((stream < 0) || (stream >= MAX_RAW_STREAMS)) + return; + + if(entityNum >= 0) + { + // This is a stream that tracks an entity + // Allocate a streamSource at normal priority + cursrc = S_AL_SrcAlloc(SRCPRI_ENTITY, entityNum, 0); + if(cursrc < 0) + return; + + S_AL_SrcSetup(cursrc, -1, SRCPRI_ENTITY, entityNum, 0, false); + alsrc = S_AL_SrcGet(cursrc); + srcList[cursrc].isTracking = true; + srcList[cursrc].isStream = true; + } + else + { + // Unspatialized stream source + + // Allocate a streamSource at high priority + cursrc = S_AL_SrcAlloc(SRCPRI_STREAM, -2, 0); + if(cursrc < 0) + return; + + alsrc = S_AL_SrcGet(cursrc); + + // Lock the streamSource so nobody else can use it, and get the raw streamSource + S_AL_SrcLock(cursrc); + + // make sure that after unmuting the S_AL_Gain in S_Update() does not turn + // volume up prematurely for this source + srcList[cursrc].scaleGain = 0.0f; + + // Set some streamSource parameters + qalSourcei (alsrc, AL_BUFFER, 0 ); + qalSourcei (alsrc, AL_LOOPING, AL_FALSE ); + qalSource3f(alsrc, AL_POSITION, 0.0, 0.0, 0.0); + qalSource3f(alsrc, AL_VELOCITY, 0.0, 0.0, 0.0); + qalSource3f(alsrc, AL_DIRECTION, 0.0, 0.0, 0.0); + qalSourcef (alsrc, AL_ROLLOFF_FACTOR, 0.0 ); + qalSourcei (alsrc, AL_SOURCE_RELATIVE, AL_TRUE ); + } + + streamSourceHandles[stream] = cursrc; + streamSources[stream] = alsrc; + + streamNumBuffers[stream] = 0; + streamBufIndex[stream] = 0; +} + +/* +================= +S_AL_FreeStreamChannel +================= +*/ +static void S_AL_FreeStreamChannel( int stream ) +{ + if ((stream < 0) || (stream >= MAX_RAW_STREAMS)) + return; + + // Detach any buffers + qalSourcei(streamSources[stream], AL_BUFFER, 0); + + // Delete the buffers + if (streamNumBuffers[stream] > 0) { + qalDeleteBuffers(streamNumBuffers[stream], streamBuffers[stream]); + streamNumBuffers[stream] = 0; + } + + // Release the output streamSource + S_AL_SrcUnlock(streamSourceHandles[stream]); + S_AL_SrcKill(streamSourceHandles[stream]); + streamSources[stream] = 0; + streamSourceHandles[stream] = -1; +} + +/* +================= +S_AL_RawSamples +================= +*/ +static +void S_AL_RawSamples(int stream, int samples, int rate, int width, int channels, const byte *data, float volume, int entityNum) +{ + int numBuffers; + ALuint buffer; + ALuint format; + + if ((stream < 0) || (stream >= MAX_RAW_STREAMS)) + return; + + format = S_AL_Format( width, channels ); + + // Create the streamSource if necessary + if(streamSourceHandles[stream] == -1) + { + S_AL_AllocateStreamChannel(stream, entityNum); + + // Failed? + if(streamSourceHandles[stream] == -1) + { + Com_Printf( S_COLOR_RED "ERROR: Can't allocate streaming streamSource\n"); + return; + } + } + + qalGetSourcei(streamSources[stream], AL_BUFFERS_QUEUED, &numBuffers); + + if (numBuffers == MAX_STREAM_BUFFERS) + { + Com_DPrintf(S_COLOR_RED"WARNING: Steam dropping raw samples, reached MAX_STREAM_BUFFERS\n"); + return; + } + + // Allocate a new AL buffer if needed + if (numBuffers == streamNumBuffers[stream]) + { + ALuint oldBuffers[MAX_STREAM_BUFFERS]; + int i; + + if (!S_AL_GenBuffers(1, &buffer, "stream")) + return; + + ::memcpy(oldBuffers, &streamBuffers[stream], sizeof (oldBuffers)); + + // Reorder buffer array in order of oldest to newest + for ( i = 0; i < streamNumBuffers[stream]; ++i ) + streamBuffers[stream][i] = oldBuffers[(streamBufIndex[stream] + i) % streamNumBuffers[stream]]; + + // Add the new buffer to end + streamBuffers[stream][streamNumBuffers[stream]] = buffer; + streamBufIndex[stream] = streamNumBuffers[stream]; + streamNumBuffers[stream]++; + } + + // Select next buffer in loop + buffer = streamBuffers[stream][ streamBufIndex[stream] ]; + streamBufIndex[stream] = (streamBufIndex[stream] + 1) % streamNumBuffers[stream]; + + // Fill buffer + qalBufferData(buffer, format, (ALvoid *)data, (samples * width * channels), rate); + + // Shove the data onto the streamSource + qalSourceQueueBuffers(streamSources[stream], 1, &buffer); + + if(entityNum < 0) + { + // Volume + S_AL_Gain (streamSources[stream], volume * s_volume->value * s_alGain->value); + } + + // Start stream + if(!streamPlaying[stream]) + { + qalSourcePlay( streamSources[stream] ); + streamPlaying[stream] = true; + } +} + +/* +================= +S_AL_StreamUpdate +================= +*/ +static +void S_AL_StreamUpdate( int stream ) +{ + int numBuffers; + ALint state; + + if ((stream < 0) || (stream >= MAX_RAW_STREAMS)) + return; + + if(streamSourceHandles[stream] == -1) + return; + + // Un-queue any buffers + qalGetSourcei( streamSources[stream], AL_BUFFERS_PROCESSED, &numBuffers ); + while( numBuffers-- ) + { + ALuint buffer; + qalSourceUnqueueBuffers(streamSources[stream], 1, &buffer); + } + + // Start the streamSource playing if necessary + qalGetSourcei( streamSources[stream], AL_BUFFERS_QUEUED, &numBuffers ); + + qalGetSourcei(streamSources[stream], AL_SOURCE_STATE, &state); + if(state == AL_STOPPED) + { + streamPlaying[stream] = false; + + // If there are no buffers queued up, release the streamSource + if( !numBuffers ) + S_AL_FreeStreamChannel( stream ); + } + + if( !streamPlaying[stream] && numBuffers ) + { + qalSourcePlay( streamSources[stream] ); + streamPlaying[stream] = true; + } +} + +/* +================= +S_AL_StreamDie +================= +*/ +static +void S_AL_StreamDie( int stream ) +{ + if ((stream < 0) || (stream >= MAX_RAW_STREAMS)) + return; + + if(streamSourceHandles[stream] == -1) + return; + + streamPlaying[stream] = false; + qalSourceStop(streamSources[stream]); + + S_AL_FreeStreamChannel(stream); +} + + +//=========================================================================== + + +#define NUM_MUSIC_BUFFERS 4 +#define MUSIC_BUFFER_SIZE 4096 + +static bool musicPlaying = false; +static srcHandle_t musicSourceHandle = -1; +static ALuint musicSource; +static ALuint musicBuffers[NUM_MUSIC_BUFFERS]; + +static snd_stream_t *mus_stream; +static snd_stream_t *intro_stream; +static char s_backgroundLoop[MAX_QPATH]; + +static byte decode_buffer[MUSIC_BUFFER_SIZE]; + +/* +================= +S_AL_MusicSourceGet +================= +*/ +static void S_AL_MusicSourceGet( void ) +{ + // Allocate a musicSource at high priority + musicSourceHandle = S_AL_SrcAlloc(SRCPRI_STREAM, -2, 0); + if(musicSourceHandle == -1) + return; + + // Lock the musicSource so nobody else can use it, and get the raw musicSource + S_AL_SrcLock(musicSourceHandle); + musicSource = S_AL_SrcGet(musicSourceHandle); + + // make sure that after unmuting the S_AL_Gain in S_Update() does not turn + // volume up prematurely for this source + srcList[musicSourceHandle].scaleGain = 0.0f; + + // Set some musicSource parameters + qalSource3f(musicSource, AL_POSITION, 0.0, 0.0, 0.0); + qalSource3f(musicSource, AL_VELOCITY, 0.0, 0.0, 0.0); + qalSource3f(musicSource, AL_DIRECTION, 0.0, 0.0, 0.0); + qalSourcef (musicSource, AL_ROLLOFF_FACTOR, 0.0 ); + qalSourcei (musicSource, AL_SOURCE_RELATIVE, AL_TRUE ); +} + +/* +================= +S_AL_MusicSourceFree +================= +*/ +static void S_AL_MusicSourceFree( void ) +{ + // Release the output musicSource + S_AL_SrcUnlock(musicSourceHandle); + S_AL_SrcKill(musicSourceHandle); + musicSource = 0; + musicSourceHandle = -1; +} + +/* +================= +S_AL_CloseMusicFiles +================= +*/ +static void S_AL_CloseMusicFiles(void) +{ + if(intro_stream) + { + S_CodecCloseStream(intro_stream); + intro_stream = NULL; + } + + if(mus_stream) + { + S_CodecCloseStream(mus_stream); + mus_stream = NULL; + } +} + +/* +================= +S_AL_StopBackgroundTrack +================= +*/ +static +void S_AL_StopBackgroundTrack( void ) +{ + if(!musicPlaying) + return; + + // Stop playing + qalSourceStop(musicSource); + + // Detach any buffers + qalSourcei(musicSource, AL_BUFFER, 0); + + // Delete the buffers + qalDeleteBuffers(NUM_MUSIC_BUFFERS, musicBuffers); + + // Free the musicSource + S_AL_MusicSourceFree(); + + // Unload the stream + S_AL_CloseMusicFiles(); + + musicPlaying = false; +} + +/* +================= +S_AL_MusicProcess +================= +*/ +static +void S_AL_MusicProcess(ALuint b) +{ + ALenum error; + int l; + ALuint format; + snd_stream_t *curstream; + + S_AL_ClearError( false ); + + if(intro_stream) + curstream = intro_stream; + else + curstream = mus_stream; + + if(!curstream) + return; + + l = S_CodecReadStream(curstream, MUSIC_BUFFER_SIZE, decode_buffer); + + // Run out data to read, start at the beginning again + if(l == 0) + { + S_CodecCloseStream(curstream); + + // the intro stream just finished playing so we don't need to reopen + // the music stream. + if(intro_stream) + intro_stream = NULL; + else + mus_stream = S_CodecOpenStream(s_backgroundLoop); + + curstream = mus_stream; + + if(!curstream) + { + S_AL_StopBackgroundTrack(); + return; + } + + l = S_CodecReadStream(curstream, MUSIC_BUFFER_SIZE, decode_buffer); + } + + format = S_AL_Format(curstream->info.width, curstream->info.channels); + + if( l == 0 ) + { + // We have no data to buffer, so buffer silence + byte dummyData[ 2 ] = { 0 }; + + qalBufferData( b, AL_FORMAT_MONO16, (void *)dummyData, 2, 22050 ); + } + else + qalBufferData(b, format, decode_buffer, l, curstream->info.rate); + + if( ( error = qalGetError( ) ) != AL_NO_ERROR ) + { + S_AL_StopBackgroundTrack( ); + Com_Printf( S_COLOR_RED "ERROR: while buffering data for music stream - %s\n", + S_AL_ErrorMsg( error ) ); + return; + } +} + +/* +================= +S_AL_StartBackgroundTrack +================= +*/ +static +void S_AL_StartBackgroundTrack( const char *intro, const char *loop ) +{ + int i; + bool issame; + + // Stop any existing music that might be playing + S_AL_StopBackgroundTrack(); + + if((!intro || !*intro) && (!loop || !*loop)) + return; + + // Allocate a musicSource + S_AL_MusicSourceGet(); + if(musicSourceHandle == -1) + return; + + if (!loop || !*loop) + { + loop = intro; + issame = true; + } + else if(intro && *intro && !strcmp(intro, loop)) + issame = true; + else + issame = false; + + // Copy the loop over + Q_strncpyz( s_backgroundLoop, loop, sizeof( s_backgroundLoop ) ); + + if(!issame) + { + // Open the intro and don't mind whether it succeeds. + // The important part is the loop. + intro_stream = S_CodecOpenStream(intro); + } + else + intro_stream = NULL; + + mus_stream = S_CodecOpenStream(s_backgroundLoop); + if(!mus_stream) + { + S_AL_CloseMusicFiles(); + S_AL_MusicSourceFree(); + return; + } + + // Generate the musicBuffers + if (!S_AL_GenBuffers(NUM_MUSIC_BUFFERS, musicBuffers, "music")) + return; + + // Queue the musicBuffers up + for(i = 0; i < NUM_MUSIC_BUFFERS; i++) + { + S_AL_MusicProcess(musicBuffers[i]); + } + + qalSourceQueueBuffers(musicSource, NUM_MUSIC_BUFFERS, musicBuffers); + + // Set the initial gain property + S_AL_Gain(musicSource, s_alGain->value * s_musicVolume->value); + + // Start playing + qalSourcePlay(musicSource); + + musicPlaying = true; +} + +/* +================= +S_AL_MusicUpdate +================= +*/ +static +void S_AL_MusicUpdate( void ) +{ + int numBuffers; + ALint state; + + if(!musicPlaying) + return; + + qalGetSourcei( musicSource, AL_BUFFERS_PROCESSED, &numBuffers ); + while( numBuffers-- ) + { + ALuint b; + qalSourceUnqueueBuffers(musicSource, 1, &b); + S_AL_MusicProcess(b); + qalSourceQueueBuffers(musicSource, 1, &b); + } + + // Hitches can cause OpenAL to be starved of buffers when streaming. + // If this happens, it will stop playback. This restarts the source if + // it is no longer playing, and if there are buffers available + qalGetSourcei( musicSource, AL_SOURCE_STATE, &state ); + qalGetSourcei( musicSource, AL_BUFFERS_QUEUED, &numBuffers ); + if( state == AL_STOPPED && numBuffers ) + { + Com_DPrintf( S_COLOR_YELLOW "Restarted OpenAL music\n" ); + qalSourcePlay(musicSource); + } + + // Set the gain property + S_AL_Gain(musicSource, s_alGain->value * s_musicVolume->value); +} + + +//=========================================================================== + + +// Local state variables +static ALCdevice *alDevice; +static ALCcontext *alContext; + +#ifdef USE_VOIP +static ALCdevice *alCaptureDevice; +static cvar_t *s_alCapture; +#endif + +#ifdef _WIN32 +#define ALDRIVER_DEFAULT "OpenAL32.dll" +#elif defined(__APPLE__) +#define ALDRIVER_DEFAULT "/System/Library/Frameworks/OpenAL.framework/OpenAL" +#elif defined(__OpenBSD__) +#define ALDRIVER_DEFAULT "libopenal.so" +#else +#define ALDRIVER_DEFAULT "libopenal.so.1" +#endif + +/* +================= +S_AL_StopAllSounds +================= +*/ +static +void S_AL_StopAllSounds( void ) +{ + int i; + S_AL_SrcShutup(); + S_AL_StopBackgroundTrack(); + for (i = 0; i < MAX_RAW_STREAMS; i++) + S_AL_StreamDie(i); +} + +/* +================= +S_AL_Respatialize +================= +*/ +static +void S_AL_Respatialize( int entityNum, const vec3_t origin, vec3_t axis[3], int inwater ) +{ + float orientation[6]; + vec3_t sorigin; + + VectorCopy( origin, sorigin ); + S_AL_SanitiseVector( sorigin ); + + S_AL_SanitiseVector( axis[ 0 ] ); + S_AL_SanitiseVector( axis[ 1 ] ); + S_AL_SanitiseVector( axis[ 2 ] ); + + orientation[0] = axis[0][0]; orientation[1] = axis[0][1]; orientation[2] = axis[0][2]; + orientation[3] = axis[2][0]; orientation[4] = axis[2][1]; orientation[5] = axis[2][2]; + + lastListenerNumber = entityNum; + VectorCopy( sorigin, lastListenerOrigin ); + + // Set OpenAL listener paramaters + qalListenerfv(AL_POSITION, (ALfloat *)sorigin); + qalListenerfv(AL_VELOCITY, vec3_origin); + qalListenerfv(AL_ORIENTATION, orientation); +} + +/* +================= +S_AL_Update +================= +*/ +static +void S_AL_Update( void ) +{ + int i; + + if(s_muted->modified) + { + // muted state changed. Let S_AL_Gain turn up all sources again. + for(i = 0; i < srcCount; i++) + { + if(srcList[i].isActive) + S_AL_Gain(srcList[i].alSource, srcList[i].scaleGain); + } + + s_muted->modified = false; + } + + // Update SFX channels + S_AL_SrcUpdate(); + + // Update streams + for (i = 0; i < MAX_RAW_STREAMS; i++) + S_AL_StreamUpdate(i); + S_AL_MusicUpdate(); + + // Doppler + if(s_doppler->modified) + { + s_alDopplerFactor->modified = true; + s_doppler->modified = false; + } + + // Doppler parameters + if(s_alDopplerFactor->modified) + { + if(s_doppler->integer) + qalDopplerFactor(s_alDopplerFactor->value); + else + qalDopplerFactor(0.0f); + s_alDopplerFactor->modified = false; + } + if(s_alDopplerSpeed->modified) + { + qalSpeedOfSound(s_alDopplerSpeed->value); + s_alDopplerSpeed->modified = false; + } + + // Clear the modified flags on the other cvars + s_alGain->modified = false; + s_volume->modified = false; + s_musicVolume->modified = false; + s_alMinDistance->modified = false; + s_alRolloff->modified = false; +} + +/* +================= +S_AL_DisableSounds +================= +*/ +static +void S_AL_DisableSounds( void ) +{ + S_AL_StopAllSounds(); +} + +/* +================= +S_AL_BeginRegistration +================= +*/ +static +void S_AL_BeginRegistration( void ) +{ + if(!numSfx) + S_AL_BufferInit(); +} + +/* +================= +S_AL_ClearSoundBuffer +================= +*/ +static +void S_AL_ClearSoundBuffer( void ) +{ +} + +/* +================= +S_AL_SoundList +================= +*/ +static +void S_AL_SoundList( void ) +{ +} + +#ifdef USE_VOIP +static +void S_AL_StartCapture( void ) +{ + if (alCaptureDevice != NULL) + qalcCaptureStart(alCaptureDevice); +} + +static +int S_AL_AvailableCaptureSamples( void ) +{ + int retval = 0; + if (alCaptureDevice != NULL) + { + ALint samples = 0; + qalcGetIntegerv(alCaptureDevice, ALC_CAPTURE_SAMPLES, sizeof (samples), &samples); + retval = (int) samples; + } + return retval; +} + +static +void S_AL_Capture( int samples, byte *data ) +{ + if (alCaptureDevice != NULL) + qalcCaptureSamples(alCaptureDevice, data, samples); +} + +void S_AL_StopCapture( void ) +{ + if (alCaptureDevice != NULL) + qalcCaptureStop(alCaptureDevice); +} + +void S_AL_MasterGain( float gain ) +{ + qalListenerf(AL_GAIN, gain); +} +#endif + + +/* +================= +S_AL_SoundInfo +================= +*/ +static void S_AL_SoundInfo(void) +{ + Com_Printf( "OpenAL info:\n" ); + Com_Printf( " Vendor: %s\n", qalGetString( AL_VENDOR ) ); + Com_Printf( " Version: %s\n", qalGetString( AL_VERSION ) ); + Com_Printf( " Renderer: %s\n", qalGetString( AL_RENDERER ) ); + Com_Printf( " AL Extensions: %s\n", qalGetString( AL_EXTENSIONS ) ); + Com_Printf( " ALC Extensions: %s\n", qalcGetString( alDevice, ALC_EXTENSIONS ) ); + + if(enumeration_all_ext) + Com_Printf(" Device: %s\n", qalcGetString(alDevice, ALC_ALL_DEVICES_SPECIFIER)); + else if(enumeration_ext) + Com_Printf(" Device: %s\n", qalcGetString(alDevice, ALC_DEVICE_SPECIFIER)); + + if(enumeration_all_ext || enumeration_ext) + Com_Printf(" Available Devices:\n%s", s_alAvailableDevices->string); + +#ifdef USE_VOIP + if(capture_ext) + { + Com_Printf(" Input Device: %s\n", qalcGetString(alCaptureDevice, ALC_CAPTURE_DEVICE_SPECIFIER)); + Com_Printf(" Available Input Devices:\n%s", s_alAvailableInputDevices->string); + } +#endif +} + + + +/* +================= +S_AL_Shutdown +================= +*/ +static +void S_AL_Shutdown( void ) +{ + // Shut down everything + int i; + for (i = 0; i < MAX_RAW_STREAMS; i++) + S_AL_StreamDie(i); + S_AL_StopBackgroundTrack( ); + S_AL_SrcShutdown( ); + S_AL_BufferShutdown( ); + + qalcDestroyContext(alContext); + qalcCloseDevice(alDevice); + +#ifdef USE_VOIP + if (alCaptureDevice != NULL) { + qalcCaptureStop(alCaptureDevice); + qalcCaptureCloseDevice(alCaptureDevice); + alCaptureDevice = NULL; + Com_Printf( "OpenAL capture device closed.\n" ); + } +#endif + + for (i = 0; i < MAX_RAW_STREAMS; i++) { + streamSourceHandles[i] = -1; + streamPlaying[i] = false; + streamSources[i] = 0; + } + + QAL_Shutdown(); +} + +#endif + +/* +================= +S_AL_Init +================= +*/ +bool S_AL_Init( soundInterface_t *si ) +{ +#ifdef USE_OPENAL + const char* device = NULL; + const char* inputdevice = NULL; + int i; + + if( !si ) { + return false; + } + + for (i = 0; i < MAX_RAW_STREAMS; i++) { + streamSourceHandles[i] = -1; + streamPlaying[i] = false; + streamSources[i] = 0; + streamNumBuffers[i] = 0; + streamBufIndex[i] = 0; + } + + // New console variables + s_alPrecache = Cvar_Get( "s_alPrecache", "1", CVAR_ARCHIVE ); + s_alGain = Cvar_Get( "s_alGain", "1.0", CVAR_ARCHIVE ); + s_alSources = Cvar_Get( "s_alSources", "96", CVAR_ARCHIVE ); + s_alDopplerFactor = Cvar_Get( "s_alDopplerFactor", "1.0", CVAR_ARCHIVE ); + s_alDopplerSpeed = Cvar_Get( "s_alDopplerSpeed", "9000", CVAR_ARCHIVE ); + s_alMinDistance = Cvar_Get( "s_alMinDistance", "120", CVAR_CHEAT ); + s_alMaxDistance = Cvar_Get("s_alMaxDistance", "1024", CVAR_CHEAT); + s_alRolloff = Cvar_Get( "s_alRolloff", "2", CVAR_CHEAT); + s_alGraceDistance = Cvar_Get("s_alGraceDistance", "512", CVAR_CHEAT); + + + s_alInputDevice = Cvar_Get( "s_alInputDevice", "", CVAR_ARCHIVE | CVAR_LATCH ); + s_alDevice = Cvar_Get("s_alDevice", "", CVAR_ARCHIVE | CVAR_LATCH); + s_alDriver = Cvar_Get( "s_alDriver", ALDRIVER_DEFAULT, CVAR_ARCHIVE | CVAR_LATCH | CVAR_PROTECTED); + + // Load QAL + if( !QAL_Init( s_alDriver->string ) ) + { + Com_Printf( "Failed to load library: \"%s\".\n", s_alDriver->string ); + if( !Q_stricmp( s_alDriver->string, ALDRIVER_DEFAULT ) || !QAL_Init( ALDRIVER_DEFAULT ) ) { + return false; + } + } + + device = s_alDevice->string; + if(device && !*device) + device = NULL; + + inputdevice = s_alInputDevice->string; + if(inputdevice && !*inputdevice) + inputdevice = NULL; + + + // Device enumeration support + enumeration_all_ext = qalcIsExtensionPresent(NULL, "ALC_ENUMERATE_ALL_EXT"); + enumeration_ext = qalcIsExtensionPresent(NULL, "ALC_ENUMERATION_EXT"); + + if(enumeration_ext || enumeration_all_ext) + { + char devicenames[16384] = ""; + const char *devicelist; +#ifdef _WIN32 + const char *defaultdevice; +#endif + int curlen; + + // get all available devices + the default device name. + if(enumeration_all_ext) + { + devicelist = qalcGetString(NULL, ALC_ALL_DEVICES_SPECIFIER); +#ifdef _WIN32 + defaultdevice = qalcGetString(NULL, ALC_DEFAULT_ALL_DEVICES_SPECIFIER); +#endif + } + else + { + // We don't have ALC_ENUMERATE_ALL_EXT but normal enumeration. + devicelist = qalcGetString(NULL, ALC_DEVICE_SPECIFIER); +#ifdef _WIN32 + defaultdevice = qalcGetString(NULL, ALC_DEFAULT_DEVICE_SPECIFIER); +#endif + enumeration_ext = true; + } + +#ifdef _WIN32 + // check whether the default device is generic hardware. If it is, change to + // Generic Software as that one works more reliably with various sound systems. + // If it's not, use OpenAL's default selection as we don't want to ignore + // native hardware acceleration. + if(!device && defaultdevice && !strcmp(defaultdevice, "Generic Hardware")) + device = "Generic Software"; +#endif + + // dump a list of available devices to a cvar for the user to see. + + if(devicelist) + { + while((curlen = strlen(devicelist))) + { + Q_strcat(devicenames, sizeof(devicenames), devicelist); + Q_strcat(devicenames, sizeof(devicenames), "\n"); + + devicelist += curlen + 1; + } + } + + s_alAvailableDevices = Cvar_Get("s_alAvailableDevices", devicenames, CVAR_ROM | CVAR_NORESTART); + } + + alDevice = qalcOpenDevice(device); + if( !alDevice && device ) + { + Com_Printf( "Failed to open OpenAL device '%s', trying default.\n", device ); + alDevice = qalcOpenDevice(NULL); + } + + if( !alDevice ) + { + QAL_Shutdown( ); + Com_Printf( "Failed to open OpenAL device.\n" ); + return false; + } + + // Create OpenAL context + alContext = qalcCreateContext( alDevice, NULL ); + if( !alContext ) + { + QAL_Shutdown( ); + qalcCloseDevice( alDevice ); + Com_Printf( "Failed to create OpenAL context.\n" ); + return false; + } + qalcMakeContextCurrent( alContext ); + + // Initialize sources, buffers, music + S_AL_BufferInit( ); + S_AL_SrcInit( ); + + // Set up OpenAL parameters (doppler, etc) + qalDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); + qalDopplerFactor( s_alDopplerFactor->value ); + qalSpeedOfSound( s_alDopplerSpeed->value ); + +#ifdef USE_VOIP + // !!! FIXME: some of these alcCaptureOpenDevice() values should be cvars. + // !!! FIXME: add support for capture device enumeration. + // !!! FIXME: add some better error reporting. + s_alCapture = Cvar_Get( "s_alCapture", "1", CVAR_ARCHIVE | CVAR_LATCH ); + if (!s_alCapture->integer) + { + Com_Printf("OpenAL capture support disabled by user ('+set s_alCapture 1' to enable)\n"); + } +#if USE_MUMBLE + else if (cl_useMumble->integer) + { + Com_Printf("OpenAL capture support disabled for Mumble support\n"); + } +#endif + else + { +#ifdef __APPLE__ + // !!! FIXME: Apple has a 1.1-compliant OpenAL, which includes + // !!! FIXME: capture support, but they don't list it in the + // !!! FIXME: extension string. We need to check the version string, + // !!! FIXME: then the extension string, but that's too much trouble, + // !!! FIXME: so we'll just check the function pointer for now. + if (qalcCaptureOpenDevice == NULL) +#else + if (!qalcIsExtensionPresent(NULL, "ALC_EXT_capture")) +#endif + { + Com_Printf("No ALC_EXT_capture support, can't record audio.\n"); + } + else + { + char inputdevicenames[16384] = ""; + const char *inputdevicelist; + const char *defaultinputdevice; + int curlen; + + capture_ext = true; + + // get all available input devices + the default input device name. + inputdevicelist = qalcGetString(NULL, ALC_CAPTURE_DEVICE_SPECIFIER); + defaultinputdevice = qalcGetString(NULL, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER); + + // dump a list of available devices to a cvar for the user to see. + if (inputdevicelist) + { + while((curlen = strlen(inputdevicelist))) + { + Q_strcat(inputdevicenames, sizeof(inputdevicenames), inputdevicelist); + Q_strcat(inputdevicenames, sizeof(inputdevicenames), "\n"); + inputdevicelist += curlen + 1; + } + } + + s_alAvailableInputDevices = Cvar_Get("s_alAvailableInputDevices", inputdevicenames, CVAR_ROM | CVAR_NORESTART); + + Com_Printf("OpenAL default capture device is '%s'\n", defaultinputdevice ? defaultinputdevice : "none"); + alCaptureDevice = qalcCaptureOpenDevice(inputdevice, 48000, AL_FORMAT_MONO16, VOIP_MAX_PACKET_SAMPLES*4); + if( !alCaptureDevice && inputdevice ) + { + Com_Printf( "Failed to open OpenAL Input device '%s', trying default.\n", inputdevice ); + alCaptureDevice = qalcCaptureOpenDevice(NULL, 48000, AL_FORMAT_MONO16, VOIP_MAX_PACKET_SAMPLES*4); + } + Com_Printf( "OpenAL capture device %s.\n", + (alCaptureDevice == NULL) ? "failed to open" : "opened"); + } + } +#endif + + si->Shutdown = S_AL_Shutdown; + si->StartSound = S_AL_StartSound; + si->StartLocalSound = S_AL_StartLocalSound; + si->StartBackgroundTrack = S_AL_StartBackgroundTrack; + si->StopBackgroundTrack = S_AL_StopBackgroundTrack; + si->RawSamples = S_AL_RawSamples; + si->StopAllSounds = S_AL_StopAllSounds; + si->ClearLoopingSounds = S_AL_ClearLoopingSounds; + si->AddLoopingSound = S_AL_AddLoopingSound; + si->AddRealLoopingSound = S_AL_AddRealLoopingSound; + si->StopLoopingSound = S_AL_StopLoopingSound; + si->Respatialize = S_AL_Respatialize; + si->UpdateEntityPosition = S_AL_UpdateEntityPosition; + si->Update = S_AL_Update; + si->DisableSounds = S_AL_DisableSounds; + si->BeginRegistration = S_AL_BeginRegistration; + si->RegisterSound = S_AL_RegisterSound; + si->SoundDuration = S_AL_SoundDuration; + si->ClearSoundBuffer = S_AL_ClearSoundBuffer; + si->SoundInfo = S_AL_SoundInfo; + si->SoundList = S_AL_SoundList; + +#ifdef USE_VOIP + si->StartCapture = S_AL_StartCapture; + si->AvailableCaptureSamples = S_AL_AvailableCaptureSamples; + si->Capture = S_AL_Capture; + si->StopCapture = S_AL_StopCapture; + si->MasterGain = S_AL_MasterGain; +#endif + + return true; +#else + return false; +#endif +} diff --git a/src/client/snd_public.h b/src/client/snd_public.h new file mode 100644 index 0000000..b8e97b9 --- /dev/null +++ b/src/client/snd_public.h @@ -0,0 +1,88 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#ifndef _SND_PUBLIC_H_ +#define _SND_PUBLIC_H_ + +void S_Init( void ); +void S_Shutdown( void ); + +// if origin is NULL, the sound will be dynamically sourced from the entity +void S_StartSound( vec3_t origin, int entnum, int entchannel, sfxHandle_t sfx ); +void S_StartLocalSound( sfxHandle_t sfx, int channelNum ); + +void S_StartBackgroundTrack( const char *intro, const char *loop ); +void S_StopBackgroundTrack( void ); + +// cinematics and voice-over-network will send raw samples +// 1.0 volume will be direct output of source samples +void S_RawSamples(int stream, int samples, int rate, int width, int channels, + const byte *data, float volume, int entityNum); + +// stop all sounds and the background track +void S_StopAllSounds( void ); + +// all continuous looping sounds must be added before calling S_Update +void S_ClearLoopingSounds( bool killall ); +void S_AddLoopingSound( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfx ); +void S_AddRealLoopingSound( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfx ); +void S_StopLoopingSound(int entityNum ); + +// recompute the relative volumes for all running sounds +// relative to the given entityNum / orientation +void S_Respatialize( int entityNum, const vec3_t origin, vec3_t axis[3], int inwater ); + +// let the sound system know where an entity currently is +void S_UpdateEntityPosition( int entityNum, const vec3_t origin ); + +void S_Update( void ); + +void S_DisableSounds( void ); + +void S_BeginRegistration( void ); + +// RegisterSound will allways return a valid sample, even if it +// has to create a placeholder. This prevents continuous filesystem +// checks for missing files +sfxHandle_t S_RegisterSound( const char *sample, bool compressed ); + +int S_SoundDuration( sfxHandle_t handle ); + +void S_DisplayFreeMemory(void); + +void S_ClearSoundBuffer( void ); + +void SNDDMA_Activate( void ); + +void S_UpdateBackgroundTrack( void ); + + +#ifdef USE_VOIP +void S_StartCapture( void ); +int S_AvailableCaptureSamples( void ); +void S_Capture( int samples, byte *data ); +void S_StopCapture( void ); +void S_MasterGain( float gain ); +#endif +#endif diff --git a/src/client/snd_wavelet.cpp b/src/client/snd_wavelet.cpp new file mode 100644 index 0000000..e06e3ef --- /dev/null +++ b/src/client/snd_wavelet.cpp @@ -0,0 +1,252 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +Copyright (C) 2015-2019 GrangerHub + +This file is part of Tremulous. + +Tremulous 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. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, see <https://www.gnu.org/licenses/> + +=========================================================================== +*/ + +#include "snd_local.h" + +#define C0 0.4829629131445341 +#define C1 0.8365163037378079 +#define C2 0.2241438680420134 +#define C3 -0.1294095225512604 + +void daub4(float b[], unsigned long n, int isign) +{ + float wksp[4097] = { 0.0f }; +#define a(x) b[(x)-1] // numerical recipies so a[1] = b[0] + + + unsigned long nh,nh1,i,j; + + if (n < 4) return; + + nh1=(nh=n >> 1)+1; + if (isign >= 0) { + for (i=1,j=1;j<=n-3;j+=2,i++) { + wksp[i] = C0*a(j)+C1*a(j+1)+C2*a(j+2)+C3*a(j+3); + wksp[i+nh] = C3*a(j)-C2*a(j+1)+C1*a(j+2)-C0*a(j+3); + } + wksp[i ] = C0*a(n-1)+C1*a(n)+C2*a(1)+C3*a(2); + wksp[i+nh] = C3*a(n-1)-C2*a(n)+C1*a(1)-C0*a(2); + } else { + wksp[1] = C2*a(nh)+C1*a(n)+C0*a(1)+C3*a(nh1); + wksp[2] = C3*a(nh)-C0*a(n)+C1*a(1)-C2*a(nh1); + for (i=1,j=3;i<nh;i++) { + wksp[j++] = C2*a(i)+C1*a(i+nh)+C0*a(i+1)+C3*a(i+nh1); + wksp[j++] = C3*a(i)-C0*a(i+nh)+C1*a(i+1)-C2*a(i+nh1); + } + } + for (i=1;i<=n;i++) { + a(i) = wksp[i]; + } +} + +void wt1(float a[], unsigned long n, int isign) +{ + unsigned long nn; + int inverseStartLength = n/4; + if (n < inverseStartLength) return; + if (isign >= 0) { + for (nn=n;nn>=inverseStartLength;nn>>=1) daub4(a,nn,isign); + } else { + for (nn=inverseStartLength;nn<=n;nn<<=1) daub4(a,nn,isign); + } +} + +/* The number of bits required by each value */ +static unsigned char numBits[] = { + 0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, +}; + +byte MuLawEncode(short s) { + unsigned long adjusted; + byte sign, exponent, mantissa; + + sign = (s<0)?0:0x80; + + if (s<0) s=-s; + adjusted = (long)s << (16-sizeof(short)*8); + adjusted += 128L + 4L; + if (adjusted > 32767) adjusted = 32767; + exponent = numBits[(adjusted>>7)&0xff] - 1; + mantissa = (adjusted>>(exponent+3))&0xf; + return ~(sign | (exponent<<4) | mantissa); +} + +short MuLawDecode(byte uLaw) { + signed long adjusted; + byte exponent, mantissa; + + uLaw = ~uLaw; + exponent = (uLaw>>4) & 0x7; + mantissa = (uLaw&0xf) + 16; + adjusted = (mantissa << (exponent +3)) - 128 - 4; + + return (uLaw & 0x80)? adjusted : -adjusted; +} + +short mulawToShort[256]; +static bool madeTable = false; + +static int NXStreamCount; + +void NXPutc(NXStream *stream, char out) { + stream[NXStreamCount++] = out; +} + + +void encodeWavelet( sfx_t *sfx, short *packets) { + float wksp[4097] = {0}, temp; + int i, samples, size; + sndBuffer *newchunk, *chunk; + byte *out; + + if (!madeTable) { + for (i=0;i<256;i++) { + mulawToShort[i] = (float)MuLawDecode((byte)i); + } + madeTable = true; + } + chunk = NULL; + + samples = sfx->soundLength; + while(samples>0) { + size = samples; + if (size>(SND_CHUNK_SIZE*2)) { + size = (SND_CHUNK_SIZE*2); + } + + if (size<4) { + size = 4; + } + + newchunk = SND_malloc(); + if (sfx->soundData == NULL) { + sfx->soundData = newchunk; + } else if (chunk != NULL) { + chunk->next = newchunk; + } + chunk = newchunk; + for(i=0; i<size; i++) { + wksp[i] = *packets; + packets++; + } + wt1(wksp, size, 1); + out = (byte *)chunk->sndChunk; + + for(i=0;i<size;i++) { + temp = wksp[i]; + if (temp > 32767) temp = 32767; else if (temp<-32768) temp = -32768; + out[i] = MuLawEncode((short)temp); + } + + chunk->size = size; + samples -= size; + } +} + +void decodeWavelet(sndBuffer *chunk, short *to) { + float wksp[4097] = {0}; + int i; + byte *out; + + int size = chunk->size; + + out = (byte *)chunk->sndChunk; + for(i=0;i<size;i++) { + wksp[i] = mulawToShort[out[i]]; + } + + wt1(wksp, size, -1); + + if (!to) return; + + for(i=0; i<size; i++) { + to[i] = wksp[i]; + } +} + + +void encodeMuLaw( sfx_t *sfx, short *packets) { + int i, samples, size, grade, poop; + sndBuffer *newchunk, *chunk; + byte *out; + + if (!madeTable) { + for (i=0;i<256;i++) { + mulawToShort[i] = (float)MuLawDecode((byte)i); + } + madeTable = true; + } + + chunk = NULL; + samples = sfx->soundLength; + grade = 0; + + while(samples>0) { + size = samples; + if (size>(SND_CHUNK_SIZE*2)) { + size = (SND_CHUNK_SIZE*2); + } + + newchunk = SND_malloc(); + if (sfx->soundData == NULL) { + sfx->soundData = newchunk; + } else if (chunk != NULL) { + chunk->next = newchunk; + } + chunk = newchunk; + out = (byte *)chunk->sndChunk; + for(i=0; i<size; i++) { + poop = packets[0]+grade; + if (poop>32767) { + poop = 32767; + } else if (poop<-32768) { + poop = -32768; + } + out[i] = MuLawEncode((short)poop); + grade = poop - mulawToShort[out[i]]; + packets++; + } + chunk->size = size; + samples -= size; + } +} + +void decodeMuLaw(sndBuffer *chunk, short *to) { + int i; + byte *out; + + int size = chunk->size; + + out = (byte *)chunk->sndChunk; + for(i=0;i<size;i++) { + to[i] = mulawToShort[out[i]]; + } +} |