/* * Copyright(c) 1997-2001 Id Software, Inc. * Copyright(c) 2002 The Quakeforge Project. * Copyright(c) 2006 Quetoo. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or(at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "client.h" #ifdef HAVE_ZLIB #include static z_stream z; static byte zbuf[MAX_MSGLEN]; #endif extern cvar_t *cl_chatsound; extern cvar_t *cl_ignore; extern cvar_t *quetoo; char *svc_strings[256] = { "svc_bad", "svc_muzzleflash", "svc_muzzlflash2", "svc_temp_entity", "svc_layout", "svc_inventory", "svc_nop", "svc_disconnect", "svc_reconnect", "svc_sound", "svc_print", "svc_stufftext", "svc_serverdata", "svc_configstring", "svc_spawnbaseline", "svc_centerprint", "svc_download", "svc_playerinfo", "svc_packetentities", "svc_deltapacketentities", "svc_frame", "svc_zpacket", "svc_zdownload", "svc_max_enttypes", "svc_zlib" }; /* ZLibDecompress A helper function for R1Q2 svc_zpacket and svc_zdownload. Deflates in to out. */ int ZLibDecompress(byte *in, int inlen, byte *out, int outlen, int wbits){ z_stream zs; memset(&zs, 0, sizeof(zs)); zs.next_in = in; zs.avail_in = 0; zs.next_out = out; zs.avail_out = outlen; inflateInit2(&zs, wbits); zs.avail_in = inlen; inflate(&zs, Z_FINISH); inflateEnd(&zs); return zs.total_out; } /* CL_DownloadFileName Prepends fs_gamedir fn so that downloads arrive in gamedir. */ void CL_DownloadFileName(char *dest, int destlen, char *fn){ Com_sprintf(dest, destlen, "%s/%s", FS_Gamedir(), fn); } /* CL_CheckOrDownloadFile Returns true if the file exists, otherwise it attempts to start a download from the server. */ qboolean CL_CheckOrDownloadFile(char *filename){ FILE *fp; char name[MAX_OSPATH]; if(filename[0] == '/'){ Com_Printf("Refusing to download a path starting with /\n"); return true; } if(strstr(filename, "..")){ Com_Printf("Refusing to download a path with ..\n"); return true; } if(strchr(filename, ' ')){ Com_Printf("Refusing to download a path with whitespace\n"); return true; } if(FS_LoadFile(filename, NULL) != -1){ // it exists, no need to download return true; } strcpy(cls.downloadname, filename); // download to a temp name, and only rename // to the real name when done, so if interrupted // a runt file wont be left COM_StripExtension(cls.downloadname, cls.downloadtempname); strcat(cls.downloadtempname, ".tmp"); //ZOID // check to see if we already have a tmp for this file, if so, try to resume // open the file if not opened yet CL_DownloadFileName(name, sizeof(name), cls.downloadtempname); fp = fopen(name, "r+b"); if(fp){ // it exists int len; fseek(fp, 0, SEEK_END); len = ftell(fp); cls.download = fp; // give the server an offset to start the download Com_Printf("Resuming %s\n", cls.downloadname); MSG_WriteByte(&cls.netchan.message, clc_stringcmd); if(cls.protocol == PROTOCOL_R1Q2) // request zlib resume MSG_WriteString(&cls.netchan.message, va("download %s %i udp-zlib", cls.downloadname, len)); else MSG_WriteString(&cls.netchan.message, va("download %s %i", cls.downloadname, len)); } else { Com_Printf("Downloading %s\n", cls.downloadname); MSG_WriteByte(&cls.netchan.message, clc_stringcmd); if(cls.protocol == PROTOCOL_R1Q2) // request zlib download MSG_WriteString(&cls.netchan.message, va("download %s 0 udp-zlib", cls.downloadname)); else MSG_WriteString(&cls.netchan.message, va("download %s", cls.downloadname)); } send_packet_now = true; cls.downloadnumber++; return false; } /* CL_Download_f Request a download from the server */ void CL_Download_f(void){ char filename[MAX_OSPATH]; if(Cmd_Argc() != 2){ Com_Printf("Usage: download \n"); return; } Com_sprintf(filename, sizeof(filename), "%s", Cmd_Argv(1)); if(strstr(filename, "..")){ Com_Printf("Refusing to download a path with ..\n"); return; } if(FS_LoadFile(filename, NULL) != -1){ // it exists, no need to download Com_Printf("File already exists.\n"); return; } strcpy(cls.downloadname, filename); Com_Printf("Downloading %s\n", cls.downloadname); // download to a temp name, and only rename // to the real name when done, so if interrupted // a runt file wont be left COM_StripExtension(cls.downloadname, cls.downloadtempname); strcat(cls.downloadtempname, ".tmp"); MSG_WriteByte(&cls.netchan.message, clc_stringcmd); if(cls.protocol == PROTOCOL_R1Q2) // request zlib download MSG_WriteString(&cls.netchan.message, va("download %s 0 udp-zlib", cls.downloadname)); else MSG_WriteString(&cls.netchan.message, va("download %s", cls.downloadname)); cls.downloadnumber++; } /* CL_ParseDownload A download message has been received from the server */ void CL_ParseDownload(qboolean dataIsCompressed){ int size, percent; char name[MAX_OSPATH]; // read the data size = MSG_ReadShort(&net_message); percent = MSG_ReadByte(&net_message); if(size < 0){ Com_Printf("Server does not have this file.\n"); if(cls.download){ // if here, we tried to resume a file but the server said no fclose(cls.download); cls.download = NULL; } CL_RequestNextDownload(); return; } // open the file if not opened yet if(!cls.download){ CL_DownloadFileName(name, sizeof(name), cls.downloadtempname); FS_CreatePath(name); cls.download = fopen(name, "wb"); if(!cls.download){ net_message.readcount += size; Com_Printf("Failed to open %s\n", cls.downloadtempname); CL_RequestNextDownload(); return; } } if(dataIsCompressed){ //svc_zdownload #ifdef HAVE_ZLIB short uncompressedLen; byte uncompressed[0xFFFF]; uncompressedLen = MSG_ReadShort(&net_message); ZLibDecompress(net_message_buffer + net_message.readcount, size, uncompressed, uncompressedLen, -15); fwrite(uncompressed, 1, uncompressedLen, cls.download); #endif } else fwrite(net_message.data + net_message.readcount, 1, size, cls.download); net_message.readcount += size; if(percent != 100){ cls.downloadpercent = percent; MSG_WriteByte(&cls.netchan.message, clc_stringcmd); SZ_Print(&cls.netchan.message, "nextdl"); send_packet_now = true; } else { char oldn[MAX_OSPATH]; char newn[MAX_OSPATH]; fclose(cls.download); // rename the temp file to it's final name CL_DownloadFileName(oldn, sizeof(oldn), cls.downloadtempname); CL_DownloadFileName(newn, sizeof(newn), cls.downloadname); if(rename(oldn, newn)) Com_Printf("failed to rename.\n"); cls.download = NULL; cls.downloadpercent = 0; // get another file if needed CL_RequestNextDownload(); } } /* SERVER CONNECTING MESSAGES */ /* CL_ParseServerData */ qboolean CL_ParseServerData(void){ extern cvar_t *fs_gamedirvar; char *str; int i, version; // wipe the client_state_t struct CL_ClearState(); cls.state = ca_connected; // parse protocol version number i = MSG_ReadLong(&net_message); cls.protocol = i; cl.servercount = MSG_ReadLong(&net_message); cl.demoserver = MSG_ReadByte(&net_message); // let demos from release work with the 3.0x patch if(i != PROTOCOL_34 && i != PROTOCOL_R1Q2 && !cl.demoserver) Com_Error(ERR_DROP, "Server is using unknown protocol %d.", i); // game directory str = MSG_ReadString(&net_message); strncpy(cl.gamedir, str, sizeof(cl.gamedir) - 1); // set gamedir if((*str && (!*fs_gamedirvar->string || strcmp(fs_gamedirvar->string, str))) || (!*str && (*fs_gamedirvar->string))){ if(strcmp(fs_gamedirvar->string, str)){ if(cl.demoserver){ Cvar_ForceSet("game", str); FS_SetGamedir(str); } else Cvar_Set("game", str); } } // parse player entity number cl.playernum = MSG_ReadShort(&net_message); // get the full level name str = MSG_ReadString(&net_message); Com_Printf("\n"); Com_Printf("%c%s\n", 2, str); if(cls.protocol == PROTOCOL_R1Q2){ cl.enhancedServer = MSG_ReadByte(&net_message); version = MSG_ReadShort(&net_message); if(version != PROTOCOL_R1Q2_MINOR){ // version mismatch Com_Printf("Protocol 35 version mismatch %d, " "retrying with protocol 34..", version); CL_Disconnect(); cls.protocol = PROTOCOL_34; CL_Reconnect_f(); return false; } if(version >= 1903){ // strafeHack was added later MSG_ReadByte(&net_message); cl.strafeHack = MSG_ReadByte(&net_message); } else cl.strafeHack = false; } else cl.enhancedServer = cl.strafeHack = false; // need to prep view at next oportunity cl.view_prepped = false; return true; } /* CL_ParseBaseline */ void CL_ParseBaseline(void){ entity_state_t *es; unsigned int bits; int newnum; entity_state_t nullstate; memset(&nullstate, 0, sizeof(nullstate)); newnum = CL_ParseEntityBits(&bits); es = &cl_entities[newnum].baseline; CL_ParseDelta(&nullstate, es, newnum, bits); } /* CL_LoadClientinfo */ void CL_LoadClientinfo(clientinfo_t *ci, char *s){ int i; char *t; char model_name[MAX_QPATH]; char skin_name[MAX_QPATH]; char model_filename[MAX_QPATH]; char skin_filename[MAX_QPATH]; char weapon_filename[MAX_QPATH]; strncpy(ci->cinfo, s, sizeof(ci->cinfo)); ci->cinfo[sizeof(ci->cinfo) - 1] = 0; // isolate the player's name strncpy(ci->name, s, sizeof(ci->name)); ci->name[sizeof(ci->name) - 1] = 0; i = 0; t = strstr(s, "\\"); if(t){ if(t - s >= sizeof(ci->name) - 1) i = -1; else { ci->name[t - s] = 0; s = t + 1; } } t = s; while(*t){ //check for non-printable chars if(*t <= 32){ i = -1; break; } t++; } if(cl_noskins->value || *s == 0 || i == -1){ Com_sprintf(model_filename, sizeof(model_filename), "players/male/tris.md2"); Com_sprintf(weapon_filename, sizeof(weapon_filename), "players/male/weapon.md2"); Com_sprintf(skin_filename, sizeof(skin_filename), "players/male/grunt.pcx"); Com_sprintf(ci->iconname, sizeof(ci->iconname), "/players/male/grunt_i.pcx"); ci->model = GL_RegisterModel(model_filename); memset(ci->weaponmodel, 0, sizeof(ci->weaponmodel)); ci->weaponmodel[0] = GL_RegisterModel(weapon_filename); ci->skin = GL_RegisterSkin(skin_filename); ci->icon = Draw_FindPic(ci->iconname); } else { // isolate the model name strcpy(model_name, s); t = strstr(model_name, "/"); if(!t) t = strstr(model_name, "\\"); if(!t) t = model_name; *t = 0; // isolate the skin name strcpy(skin_name, s + strlen(model_name) + 1); // model file Com_sprintf(model_filename, sizeof(model_filename), "players/%s/tris.md2", model_name); ci->model = GL_RegisterModel(model_filename); if(!ci->model){ strcpy(model_name, "male"); Com_sprintf(model_filename, sizeof(model_filename), "players/male/tris.md2"); ci->model = GL_RegisterModel(model_filename); } // skin file Com_sprintf(skin_filename, sizeof(skin_filename), "players/%s/%s.pcx", model_name, skin_name); ci->skin = GL_RegisterSkin(skin_filename); // if we don't have the skin and the model wasn't male, // see if the male has it (this is for CTF's skins) if(!ci->skin && Q_stricmp(model_name, "male")){ // change model to male strcpy(model_name, "male"); Com_sprintf(model_filename, sizeof(model_filename), "players/male/tris.md2"); ci->model = GL_RegisterModel(model_filename); // see if the skin exists for the male model Com_sprintf(skin_filename, sizeof(skin_filename), "players/%s/%s.pcx", model_name, skin_name); ci->skin = GL_RegisterSkin(skin_filename); } // if we still don't have a skin, it means that the male model didn't have // it, so default to grunt if(!ci->skin){ // see if the skin exists for the male model Com_sprintf(skin_filename, sizeof(skin_filename), "players/%s/grunt.pcx", model_name); ci->skin = GL_RegisterSkin(skin_filename); } // weapon file for(i = 0; i < num_cl_weaponmodels; i++){ Com_sprintf(weapon_filename, sizeof(weapon_filename), "players/%s/%s", model_name, cl_weaponmodels[i]); ci->weaponmodel[i] = GL_RegisterModel(weapon_filename); if(!ci->weaponmodel[i] && strcmp(model_name, "cyborg") == 0){ // try male Com_sprintf(weapon_filename, sizeof(weapon_filename), "players/male/%s", cl_weaponmodels[i]); ci->weaponmodel[i] = GL_RegisterModel(weapon_filename); } if(!cl_vwep->value) break; // only one when vwep is off } // icon file Com_sprintf(ci->iconname, sizeof(ci->iconname), "/players/%s/%s_i.pcx", model_name, skin_name); ci->icon = Draw_FindPic(ci->iconname); } // must have loaded all data types to be valid if(!ci->skin || !ci->icon || !ci->model || !ci->weaponmodel[0]){ ci->skin = NULL; ci->icon = NULL; ci->model = NULL; ci->weaponmodel[0] = NULL; return; } } /* CL_ParseClientinfo Load the skin, icon, and model for a client */ void CL_ParseClientinfo(int player){ char *s; clientinfo_t *ci; s = cl.configstrings[player + CS_PLAYERSKINS]; ci = &cl.clientinfo[player]; CL_LoadClientinfo(ci, s); } /* CL_ParseConfigString */ void CL_ParseConfigString(void){ int i; char *s; char olds[MAX_QPATH]; i = MSG_ReadShort(&net_message); if(i < 0 || i >= MAX_CONFIGSTRINGS) Com_Error(ERR_DROP, "configstring > MAX_CONFIGSTRINGS"); s = MSG_ReadString(&net_message); strncpy(olds, cl.configstrings[i], sizeof(olds)); olds[sizeof(olds) - 1] = 0; strcpy(cl.configstrings[i], s); // do something apropriate if(i >= CS_MODELS && i < CS_MODELS + MAX_MODELS){ if(cl.view_prepped){ cl.model_draw[i - CS_MODELS] = GL_RegisterModel(cl.configstrings[i]); if(cl.configstrings[i][0] == '*') cl.model_clip[i - CS_MODELS] = CM_InlineModel(cl.configstrings[i]); else cl.model_clip[i - CS_MODELS] = NULL; } } else if(i >= CS_SOUNDS && i < CS_SOUNDS + MAX_MODELS){ if(cl.view_prepped) cl.sound_precache[i - CS_SOUNDS] = S_RegisterSound(cl.configstrings[i]); } else if(i >= CS_IMAGES && i < CS_IMAGES + MAX_MODELS){ if(cl.view_prepped) cl.image_precache[i - CS_IMAGES] = Draw_FindPic(cl.configstrings[i]); } else if(i >= CS_PLAYERSKINS && i < CS_PLAYERSKINS + MAX_CLIENTS){ if(cl.view_prepped && strcmp(olds, s)) CL_ParseClientinfo(i - CS_PLAYERSKINS); } } /* ACTION MESSAGES */ /* CL_ParseStartSoundPacket */ void CL_ParseStartSoundPacket(void){ vec3_t pos_v; float *pos; int channel, ent; int sound_num; float volume; float attenuation; int flags; float ofs; flags = MSG_ReadByte(&net_message); sound_num = MSG_ReadByte(&net_message); if(flags & SND_VOLUME) volume = MSG_ReadByte(&net_message) / 255.0; else volume = DEFAULT_SOUND_PACKET_VOLUME; if(flags & SND_ATTENUATION) attenuation = MSG_ReadByte(&net_message) / 64.0; else attenuation = DEFAULT_SOUND_PACKET_ATTENUATION; if(flags & SND_OFFSET) ofs = MSG_ReadByte(&net_message) / 1000.0; else ofs = 0; if(flags & SND_ENT){ // entity relative channel = MSG_ReadShort(&net_message); ent = channel >> 3; if(ent > MAX_EDICTS) Com_Error(ERR_DROP, "CL_ParseStartSoundPacket: ent = %i", ent); channel &= 7; } else { ent = 0; channel = 0; } if(flags & SND_POS){ // positioned in space MSG_ReadPos(&net_message, pos_v); pos = pos_v; } else // use entity number pos = NULL; if(!cl.sound_precache[sound_num]) return; S_StartSound(pos, ent, channel, cl.sound_precache[sound_num], volume, attenuation, ofs); } /* * CL_IgnoreChatMessage * * Returns true of msg should be discarded, according to cl_ignove. */ qboolean CL_IgnoreChatMessage(char *msg){ if(!*cl_ignore->string || strlen(cl_ignore->string) < 1) return false; // nothing currently filtered char *s = strtok(cl_ignore->string, " "); while(s){ if(strstr(msg, s)) return true; s = strtok(NULL, " "); } return false; // msg is okay } /* CL_ShowNet */ void CL_ShowNet(char *s){ if(cl_shownet->value >= 2) Com_Printf("%3i:%s\n", net_message.readcount - 1, s); } /* CL_ZlibServerMessage Called for svc_zlib, this function inflates the remainder of the current net_message and adjusts its length accordingly. */ void CL_ZlibServerMessage(void){ #ifdef HAVE_ZLIB int len; memset(zbuf, 0, MAX_MSGLEN); z.zalloc = Z_NULL; z.zfree = Z_NULL; z.opaque = Z_NULL; inflateInit2(&z, -15); z.avail_in = net_message.cursize - net_message.readcount; z.next_in = net_message.data + net_message.readcount; z.avail_out = MAX_MSGLEN; z.next_out = zbuf; inflate(&z, Z_NO_FLUSH); len = MAX_MSGLEN - z.avail_out; inflateEnd(&z); net_message.readcount--; // overwrite the zlib command // clear remainder of message, replace with deflated // content, and update message length accordingly memset(net_message.data + net_message.readcount, 0, net_message.cursize - net_message.readcount); memcpy(net_message.data + net_message.readcount, zbuf, len); net_message.cursize = net_message.readcount + len; #endif } /* CL_ParseZPacket Called for svc_zpacket, this is slightly different than Quetoo's svc_zlib. For starters, more length checking is performed, and the message is not inflated in place, but a copy is used. */ void CL_ParseZPacket(void){ #ifdef HAVE_ZLIB byte buff_in[MAX_MSGLEN]; byte buff_out[0xFFFF]; sizebuf_t sb, old; short compressed_len = MSG_ReadShort(&net_message); short uncompressed_len = MSG_ReadShort(&net_message); if(uncompressed_len <= 0) Com_Error(ERR_DROP, "CL_ParseZPacket: uncompressed_len <= 0"); if(compressed_len <= 0) Com_Error(ERR_DROP, "CL_ParseZPacket: compressed_len <= 0"); MSG_ReadData(&net_message, buff_in, compressed_len); SZ_Init(&sb, buff_out, uncompressed_len); sb.cursize = ZLibDecompress(buff_in, compressed_len, buff_out, uncompressed_len, -15); old = net_message; net_message = sb; CL_ParseServerMessage(); net_message = old; #endif } /* CL_ParseServerMessage */ void CL_ParseServerMessage(void){ int cmd, oldcmd, extrabits; char *s; int i; if(cl_shownet->value == 1) Com_Printf("%i ", net_message.cursize); else if(cl_shownet->value >= 2) Com_Printf("------------------\n"); cmd = 0; // parse the message while(1){ if(net_message.readcount > net_message.cursize){ Com_Error(ERR_DROP, "CL_ParseServerMessage: Bad server message"); break; } oldcmd = cmd; cmd = MSG_ReadByte(&net_message); if(cmd == -1){ CL_ShowNet("END OF MESSAGE"); break; } extrabits = cmd & 0xE0; // mask off PROTOCOL_R1Q2 extras cmd &= 0x1F; if(cl_shownet->value >= 2 && svc_strings[cmd]) CL_ShowNet(svc_strings[cmd]); // other commands switch(cmd){ case svc_nop: break; case svc_disconnect: Com_Error(ERR_DISCONNECT, "Server disconnected.\n"); break; case svc_reconnect: Com_Printf("Server disconnected, reconnecting..\n"); if(cls.download){ // close download fclose(cls.download); cls.download = NULL; } cls.state = ca_connecting; cls.connect_time = -99999; // fire immediately break; case svc_print: i = MSG_ReadByte(&net_message); s = MSG_ReadString(&net_message); if(i == PRINT_CHAT){ if(CL_IgnoreChatMessage(s)) // filter /ignore'd chatters break; if(*cl_chatsound->string) // trigger chat sound S_StartLocalSound(cl_chatsound->string); con.ormask = 128; } Com_Printf("%s", s); con.ormask = 0; break; case svc_centerprint: SCR_CenterPrint(MSG_ReadString(&net_message)); break; case svc_stufftext: s = MSG_ReadString(&net_message); Cbuf_AddText(s); break; case svc_serverdata: Cbuf_Execute(); // make sure any stuffed commands are done if(!CL_ParseServerData()) return; break; case svc_configstring: CL_ParseConfigString(); break; case svc_sound: CL_ParseStartSoundPacket(); break; case svc_spawnbaseline: CL_ParseBaseline(); break; case svc_temp_entity: CL_ParseTEnt(); break; case svc_muzzleflash: CL_ParseMuzzleFlash(); break; case svc_muzzleflash2: CL_ParseMuzzleFlash2(); break; case svc_download: CL_ParseDownload(false); break; case svc_frame: CL_ParseFrame(extrabits); break; case svc_zpacket: CL_ParseZPacket(); break; case svc_zdownload: CL_ParseDownload(true); break; case svc_zlib: if(!((int)quetoo->value & QUETOO_ZLIB)){ // hopefully this never happens Com_Printf("CL_ParseServerMessage: zlib received but disabled\n"); return; } CL_ZlibServerMessage(); break; case svc_inventory: CL_ParseInventory(); break; case svc_layout: s = MSG_ReadString(&net_message); strncpy(cl.layout, s, sizeof(cl.layout) - 1); break; case svc_playerinfo: case svc_packetentities: case svc_deltapacketentities: Com_Error(ERR_DROP, "Out of place frame data"); break; default: Com_Printf("CL_ParseServerMessage: Illegible server message\n" " %d: last command was %s\n", cmd, svc_strings[oldcmd]); break; } } CL_AddNetgraph(); // we don't know if it is ok to save a demo message until // after we have parsed the frame if(cls.demorecording && !cls.demowaiting) CL_WriteDemoMessage(); }