/* Copyright (C) 1997-2001 Id Software, Inc. 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // sv_send.c #include "server.h" /* ============================================================================= MISC ============================================================================= */ char sv_outputbuf[SV_OUTPUTBUF_LENGTH]; void SV_FlushRedirect(int redirected, char *outputbuf, size_t len) { byte buffer[MAX_PACKETLEN_DEFAULT]; if (redirected == RD_PACKET) { memcpy(buffer, "\xff\xff\xff\xffprint\n", 10); memcpy(buffer + 10, outputbuf, len); NET_SendPacket(NS_SERVER, buffer, len + 10, &net_from); } else if (redirected == RD_CLIENT) { MSG_WriteByte(svc_print); MSG_WriteByte(PRINT_HIGH); MSG_WriteData(outputbuf, len); MSG_WriteByte(0); SV_ClientAddMessage(sv_client, MSG_RELIABLE | MSG_CLEAR); } } /* ======================= SV_RateDrop Returns qtrue if the client is over its current bandwidth estimation and should not be sent another packet ======================= */ static qboolean SV_RateDrop(client_t *client) { size_t total; int i; // never drop over the loopback if (!client->rate) { return qfalse; } total = 0; for (i = 0; i < RATE_MESSAGES; i++) { total += client->message_size[i]; } #if USE_FPS total = total * sv.framediv / client->framediv; #endif if (total > client->rate) { SV_DPrintf(0, "Frame %d suppressed for %s (total = %"PRIz")\n", client->framenum, client->name, total); client->frameflags |= FF_SUPPRESSED; client->suppress_count++; client->message_size[client->framenum % RATE_MESSAGES] = 0; return qtrue; } return qfalse; } static void SV_CalcSendTime(client_t *client, size_t size) { // never drop over the loopback if (!client->rate) { client->send_time = svs.realtime; client->send_delta = 0; return; } if (client->state == cs_spawned) client->message_size[client->framenum % RATE_MESSAGES] = size; client->send_time = svs.realtime; client->send_delta = size * 1000 / client->rate; } /* ============================================================================= EVENT MESSAGES ============================================================================= */ /* ================= SV_ClientPrintf Sends text across to be displayed if the level passes. NOT archived in MVD stream. ================= */ void SV_ClientPrintf(client_t *client, int level, const char *fmt, ...) { va_list argptr; char string[MAX_STRING_CHARS]; size_t len; if (level < client->messagelevel) return; va_start(argptr, fmt); len = Q_vsnprintf(string, sizeof(string), fmt, argptr); va_end(argptr); if (len >= sizeof(string)) { Com_WPrintf("%s: overflow\n", __func__); return; } MSG_WriteByte(svc_print); MSG_WriteByte(level); MSG_WriteData(string, len + 1); SV_ClientAddMessage(client, MSG_RELIABLE | MSG_CLEAR); } /* ================= SV_BroadcastPrintf Sends text to all active clients. NOT archived in MVD stream. ================= */ void SV_BroadcastPrintf(int level, const char *fmt, ...) { va_list argptr; char string[MAX_STRING_CHARS]; client_t *client; size_t len; va_start(argptr, fmt); len = Q_vsnprintf(string, sizeof(string), fmt, argptr); va_end(argptr); if (len >= sizeof(string)) { Com_WPrintf("%s: overflow\n", __func__); return; } MSG_WriteByte(svc_print); MSG_WriteByte(level); MSG_WriteData(string, len + 1); FOR_EACH_CLIENT(client) { if (client->state != cs_spawned) continue; if (level < client->messagelevel) continue; SV_ClientAddMessage(client, MSG_RELIABLE); } SZ_Clear(&msg_write); } void SV_ClientCommand(client_t *client, const char *fmt, ...) { va_list argptr; char string[MAX_STRING_CHARS]; size_t len; va_start(argptr, fmt); len = Q_vsnprintf(string, sizeof(string), fmt, argptr); va_end(argptr); if (len >= sizeof(string)) { Com_WPrintf("%s: overflow\n", __func__); return; } MSG_WriteByte(svc_stufftext); MSG_WriteData(string, len + 1); SV_ClientAddMessage(client, MSG_RELIABLE | MSG_CLEAR); } /* ================= SV_BroadcastCommand Sends command to all active clients. NOT archived in MVD stream. ================= */ void SV_BroadcastCommand(const char *fmt, ...) { va_list argptr; char string[MAX_STRING_CHARS]; client_t *client; size_t len; va_start(argptr, fmt); len = Q_vsnprintf(string, sizeof(string), fmt, argptr); va_end(argptr); if (len >= sizeof(string)) { Com_WPrintf("%s: overflow\n", __func__); return; } MSG_WriteByte(svc_stufftext); MSG_WriteData(string, len + 1); FOR_EACH_CLIENT(client) { SV_ClientAddMessage(client, MSG_RELIABLE); } SZ_Clear(&msg_write); } /* ================= SV_Multicast Sends the contents of the write buffer to a subset of the clients, then clears the write buffer. Archived in MVD stream. MULTICAST_ALL same as broadcast (origin can be NULL) MULTICAST_PVS send to clients potentially visible from org MULTICAST_PHS send to clients potentially hearable from org ================= */ void SV_Multicast(vec3_t origin, multicast_t to) { client_t *client; byte mask[VIS_MAX_BYTES]; mleaf_t *leaf1, *leaf2; int leafnum q_unused; int flags; vec3_t org; if (!sv.cm.cache) { Com_Error(ERR_DROP, "%s: no map loaded", __func__); } flags = 0; switch (to) { case MULTICAST_ALL_R: flags |= MSG_RELIABLE; // intentional fallthrough case MULTICAST_ALL: leaf1 = NULL; leafnum = 0; break; case MULTICAST_PHS_R: flags |= MSG_RELIABLE; // intentional fallthrough case MULTICAST_PHS: leaf1 = CM_PointLeaf(&sv.cm, origin); leafnum = leaf1 - sv.cm.cache->leafs; BSP_ClusterVis(sv.cm.cache, mask, leaf1->cluster, DVIS_PHS); break; case MULTICAST_PVS_R: flags |= MSG_RELIABLE; // intentional fallthrough case MULTICAST_PVS: leaf1 = CM_PointLeaf(&sv.cm, origin); leafnum = leaf1 - sv.cm.cache->leafs; BSP_ClusterVis(sv.cm.cache, mask, leaf1->cluster, DVIS_PVS); break; default: Com_Error(ERR_DROP, "SV_Multicast: bad to: %i", to); } // send the data to all relevent clients FOR_EACH_CLIENT(client) { if (client->state < cs_primed) { continue; } // do not send unreliables to connecting clients if (!(flags & MSG_RELIABLE) && (client->state != cs_spawned || client->download || client->nodata)) { continue; } if (leaf1) { // find the client's PVS #if 0 player_state_t *ps = &client->edict->client->ps; VectorMA(ps->viewoffset, 0.125f, ps->pmove.origin, org); #else // FIXME: for some strange reason, game code assumes the server // uses entity origin for PVS/PHS culling, not the view origin VectorCopy(client->edict->s.origin, org); #endif leaf2 = CM_PointLeaf(&sv.cm, org); if (!CM_AreasConnected(&sv.cm, leaf1->area, leaf2->area)) continue; if (leaf2->cluster == -1) continue; if (!Q_IsBitSet(mask, leaf2->cluster)) continue; } SV_ClientAddMessage(client, flags); } // add to MVD datagram SV_MvdMulticast(leafnum, to); // clear the buffer SZ_Clear(&msg_write); } static qboolean compress_message(client_t *client, int flags) { #if USE_ZLIB byte buffer[MAX_MSGLEN]; if (!(flags & MSG_COMPRESS)) return qfalse; if (!client->has_zlib) return qfalse; // older clients have problems seamlessly writing svc_zpackets if (client->settings[CLS_RECORDING]) { if (client->protocol != PROTOCOL_VERSION_Q2PRO) return qfalse; if (client->version < PROTOCOL_VERSION_Q2PRO_EXTENDED_LAYOUT) return qfalse; } // compress only sufficiently large layouts if (msg_write.cursize < client->netchan->maxpacketlen / 2) return qfalse; deflateReset(&svs.z); svs.z.next_in = msg_write.data; svs.z.avail_in = (uInt)msg_write.cursize; svs.z.next_out = buffer + 5; svs.z.avail_out = (uInt)(MAX_MSGLEN - 5); if (deflate(&svs.z, Z_FINISH) != Z_STREAM_END) return qfalse; buffer[0] = svc_zpacket; buffer[1] = svs.z.total_out & 255; buffer[2] = (svs.z.total_out >> 8) & 255; buffer[3] = msg_write.cursize & 255; buffer[4] = (msg_write.cursize >> 8) & 255; SV_DPrintf(0, "%s: comp: %lu into %lu\n", client->name, svs.z.total_in, svs.z.total_out + 5); if (svs.z.total_out + 5 > msg_write.cursize) return qfalse; client->AddMessage(client, buffer, svs.z.total_out + 5, (flags & MSG_RELIABLE) ? qtrue : qfalse); return qtrue; #else return qfalse; #endif } /* ======================= SV_ClientAddMessage Adds contents of the current write buffer to client's message list. Does NOT clean the buffer for multicast delivery purpose, unless told otherwise. ======================= */ void SV_ClientAddMessage(client_t *client, int flags) { SV_DPrintf(1, "Added %sreliable message to %s: %"PRIz" bytes\n", (flags & MSG_RELIABLE) ? "" : "un", client->name, msg_write.cursize); if (!msg_write.cursize) { return; } if (compress_message(client, flags)) { goto clear; } client->AddMessage(client, msg_write.data, msg_write.cursize, (flags & MSG_RELIABLE) ? qtrue : qfalse); clear: if (flags & MSG_CLEAR) { SZ_Clear(&msg_write); } } /* =============================================================================== FRAME UPDATES - COMMON =============================================================================== */ static inline void free_msg_packet(client_t *client, message_packet_t *msg) { List_Remove(&msg->entry); if (msg->cursize > MSG_TRESHOLD) { if (msg->cursize > client->msg_dynamic_bytes) { Com_Error(ERR_FATAL, "%s: bad packet size", __func__); } client->msg_dynamic_bytes -= msg->cursize; Z_Free(msg); } else { List_Insert(&client->msg_free_list, &msg->entry); } } #define FOR_EACH_MSG_SAFE(list) \ LIST_FOR_EACH_SAFE(message_packet_t, msg, next, list, entry) #define MSG_FIRST(list) \ LIST_FIRST(message_packet_t, list, entry) static void free_all_messages(client_t *client) { message_packet_t *msg, *next; FOR_EACH_MSG_SAFE(&client->msg_unreliable_list) { free_msg_packet(client, msg); } FOR_EACH_MSG_SAFE(&client->msg_reliable_list) { free_msg_packet(client, msg); } client->msg_unreliable_bytes = 0; client->msg_dynamic_bytes = 0; } static void add_msg_packet(client_t *client, byte *data, size_t len, qboolean reliable) { message_packet_t *msg; if (!client->msg_pool) { return; // already dropped } if (len > MSG_TRESHOLD) { if (len > MAX_MSGLEN) { Com_Error(ERR_FATAL, "%s: oversize packet", __func__); } if (client->msg_dynamic_bytes + len > MAX_MSGLEN) { Com_WPrintf("%s: %s: out of dynamic memory\n", __func__, client->name); goto overflowed; } msg = SV_Malloc(sizeof(*msg) + len - MSG_TRESHOLD); client->msg_dynamic_bytes += len; } else { if (LIST_EMPTY(&client->msg_free_list)) { Com_WPrintf("%s: %s: out of message slots\n", __func__, client->name); goto overflowed; } msg = MSG_FIRST(&client->msg_free_list); List_Remove(&msg->entry); } memcpy(msg->data, data, len); msg->cursize = (uint16_t)len; if (reliable) { List_Append(&client->msg_reliable_list, &msg->entry); } else { List_Append(&client->msg_unreliable_list, &msg->entry); client->msg_unreliable_bytes += len; } return; overflowed: if (reliable) { free_all_messages(client); SV_DropClient(client, "reliable queue overflowed"); } } // check if this entity is present in current client frame static qboolean check_entity(client_t *client, int entnum) { client_frame_t *frame; unsigned i, j; frame = &client->frames[client->framenum & UPDATE_MASK]; for (i = 0; i < frame->num_entities; i++) { j = (frame->first_entity + i) % svs.num_entities; if (svs.entities[j].number == entnum) { return qtrue; } } return qfalse; } // sounds reliative to entities are handled specially static void emit_snd(client_t *client, message_packet_t *msg) { int flags, entnum; int i; entnum = msg->sendchan >> 3; flags = msg->flags; // check if position needs to be explicitly sent if (!(flags & SND_POS) && !check_entity(client, entnum)) { SV_DPrintf(0, "Forcing position on entity %d for %s\n", entnum, client->name); flags |= SND_POS; // entity is not present in frame } MSG_WriteByte(svc_sound); MSG_WriteByte(flags); MSG_WriteByte(msg->index); if (flags & SND_VOLUME) MSG_WriteByte(msg->volume); if (flags & SND_ATTENUATION) MSG_WriteByte(msg->attenuation); if (flags & SND_OFFSET) MSG_WriteByte(msg->timeofs); MSG_WriteShort(msg->sendchan); if (flags & SND_POS) { for (i = 0; i < 3; i++) { MSG_WriteShort(msg->pos[i]); } } } static inline void write_snd(client_t *client, message_packet_t *msg, size_t maxsize) { // if this msg fits, write it if (msg_write.cursize + MAX_SOUND_PACKET <= maxsize) { emit_snd(client, msg); } List_Remove(&msg->entry); List_Insert(&client->msg_free_list, &msg->entry); } static inline void write_msg(client_t *client, message_packet_t *msg, size_t maxsize) { // if this msg fits, write it if (msg_write.cursize + msg->cursize <= maxsize) { MSG_WriteData(msg->data, msg->cursize); } free_msg_packet(client, msg); } static inline void write_unreliables(client_t *client, size_t maxsize) { message_packet_t *msg, *next; FOR_EACH_MSG_SAFE(&client->msg_unreliable_list) { if (msg->cursize) { write_msg(client, msg, maxsize); } else { write_snd(client, msg, maxsize); } } } /* =============================================================================== FRAME UPDATES - OLD NETCHAN =============================================================================== */ static void add_message_old(client_t *client, byte *data, size_t len, qboolean reliable) { if (len > client->netchan->maxpacketlen) { if (reliable) { SV_DropClient(client, "oversize reliable message"); } else { Com_DPrintf("Dumped oversize unreliable for %s\n", client->name); } return; } add_msg_packet(client, data, len, reliable); } // this should be the only place data is ever written to netchan message for old clients static void write_reliables_old(client_t *client, size_t maxsize) { message_packet_t *msg, *next; int count; if (client->netchan->reliable_length) { SV_DPrintf(1, "%s to %s: unacked\n", __func__, client->name); return; // there is still outgoing reliable message pending } // find at least one reliable message to send count = 0; FOR_EACH_MSG_SAFE(&client->msg_reliable_list) { // stop if this msg doesn't fit (reliables must be delivered in order) if (client->netchan->message.cursize + msg->cursize > maxsize) { if (!count) { // this should never happen Com_WPrintf("%s to %s: overflow on the first message\n", __func__, client->name); } break; } SV_DPrintf(1, "%s to %s: writing msg %d: %d bytes\n", __func__, client->name, count, msg->cursize); SZ_Write(&client->netchan->message, msg->data, msg->cursize); free_msg_packet(client, msg); count++; } } // unreliable portion doesn't fit, then throw out low priority effects static void repack_unreliables(client_t *client, size_t maxsize) { message_packet_t *msg, *next; if (msg_write.cursize + 4 > maxsize) { return; } // temp entities first FOR_EACH_MSG_SAFE(&client->msg_unreliable_list) { if (!msg->cursize || msg->data[0] != svc_temp_entity) { continue; } // ignore some low-priority effects, these checks come from r1q2 if (msg->data[1] == TE_BLOOD || msg->data[1] == TE_SPLASH || msg->data[1] == TE_GUNSHOT || msg->data[1] == TE_BULLET_SPARKS || msg->data[1] == TE_SHOTGUN) { continue; } write_msg(client, msg, maxsize); } if (msg_write.cursize + 4 > maxsize) { return; } // then entity sounds FOR_EACH_MSG_SAFE(&client->msg_unreliable_list) { if (!msg->cursize) { write_snd(client, msg, maxsize); } } if (msg_write.cursize + 4 > maxsize) { return; } // then positioned sounds FOR_EACH_MSG_SAFE(&client->msg_unreliable_list) { if (msg->cursize && msg->data[0] == svc_sound) { write_msg(client, msg, maxsize); } } if (msg_write.cursize + 4 > maxsize) { return; } // then everything else left FOR_EACH_MSG_SAFE(&client->msg_unreliable_list) { if (msg->cursize) { write_msg(client, msg, maxsize); } } } static void write_datagram_old(client_t *client) { message_packet_t *msg; size_t maxsize, cursize; // determine how much space is left for unreliable data maxsize = client->netchan->maxpacketlen; if (client->netchan->reliable_length) { // there is still unacked reliable message pending maxsize -= client->netchan->reliable_length; } else { // find at least one reliable message to send // and make sure to reserve space for it if (!LIST_EMPTY(&client->msg_reliable_list)) { msg = MSG_FIRST(&client->msg_reliable_list); maxsize -= msg->cursize; } } // send over all the relevant entity_state_t // and the player_state_t client->WriteFrame(client); if (msg_write.cursize > maxsize) { SV_DPrintf(0, "Frame %d overflowed for %s: %"PRIz" > %"PRIz"\n", client->framenum, client->name, msg_write.cursize, maxsize); SZ_Clear(&msg_write); } // now write unreliable messages // it is necessary for this to be after the WriteFrame // so that entity references will be current if (msg_write.cursize + client->msg_unreliable_bytes > maxsize) { // throw out some low priority effects repack_unreliables(client, maxsize); } else { // all messages fit, write them in order write_unreliables(client, maxsize); } // write at least one reliable message write_reliables_old(client, client->netchan->maxpacketlen - msg_write.cursize); // send the datagram cursize = client->netchan->Transmit(client->netchan, msg_write.cursize, msg_write.data, client->numpackets); // record the size for rate estimation SV_CalcSendTime(client, cursize); // clear the write buffer SZ_Clear(&msg_write); } /* =============================================================================== FRAME UPDATES - NEW NETCHAN =============================================================================== */ static void add_message_new(client_t *client, byte *data, size_t len, qboolean reliable) { if (reliable) { // don't packetize, netchan level will do fragmentation as needed SZ_Write(&client->netchan->message, data, len); } else { // still have to packetize, relative sounds need special processing add_msg_packet(client, data, len, qfalse); } } static void write_datagram_new(client_t *client) { size_t cursize; // send over all the relevant entity_state_t // and the player_state_t client->WriteFrame(client); if (msg_write.overflowed) { // should never really happen Com_WPrintf("Frame overflowed for %s\n", client->name); SZ_Clear(&msg_write); } // now write unreliable messages // for this client out to the message // it is necessary for this to be after the WriteFrame // so that entity references will be current if (msg_write.cursize + client->msg_unreliable_bytes > msg_write.maxsize) { Com_WPrintf("Dumping datagram for %s\n", client->name); } else { write_unreliables(client, msg_write.maxsize); } #ifdef _DEBUG if (sv_pad_packets->integer) { size_t pad = msg_write.cursize + sv_pad_packets->integer; if (pad > msg_write.maxsize) { pad = msg_write.maxsize; } for (; pad > 0; pad--) { MSG_WriteByte(svc_nop); } } #endif // send the datagram cursize = client->netchan->Transmit(client->netchan, msg_write.cursize, msg_write.data, client->numpackets); // record the size for rate estimation SV_CalcSendTime(client, cursize); // clear the write buffer SZ_Clear(&msg_write); } /* =============================================================================== COMMON STUFF =============================================================================== */ static void finish_frame(client_t *client) { message_packet_t *msg, *next; FOR_EACH_MSG_SAFE(&client->msg_unreliable_list) { free_msg_packet(client, msg); } client->msg_unreliable_bytes = 0; } #if (defined _DEBUG) && USE_FPS static void check_key_sync(client_t *client) { int div = sv.framediv / client->framediv; int key1 = !(sv.framenum % sv.framediv); int key2 = !(client->framenum % div); if (key1 != key2) { Com_LPrintf(PRINT_DEVELOPER, "[%d] frame %d for %s not synced (%d != %d)\n", sv.framenum, client->framenum, client->name, key1, key2); } } #endif /* ======================= SV_SendClientMessages Called each game frame, sends svc_frame messages to spawned clients only. Clients in earlier connection state are handled in SV_SendAsyncPackets. ======================= */ void SV_SendClientMessages(void) { client_t *client; size_t cursize; // send a message to each connected client FOR_EACH_CLIENT(client) { if (client->state != cs_spawned || client->download || client->nodata) goto finish; if (!SV_CLIENTSYNC(client)) continue; #if (defined _DEBUG) && USE_FPS if (developer->integer) check_key_sync(client); #endif // if the reliable message overflowed, // drop the client (should never happen) if (client->netchan->message.overflowed) { SZ_Clear(&client->netchan->message); SV_DropClient(client, "reliable message overflowed"); goto finish; } // don't overrun bandwidth if (SV_RateDrop(client)) goto advance; // don't write any frame data until all fragments are sent if (client->netchan->fragment_pending) { client->frameflags |= FF_SUPPRESSED; cursize = client->netchan->TransmitNextFragment(client->netchan); SV_CalcSendTime(client, cursize); goto advance; } // build the new frame and write it SV_BuildClientFrame(client); client->WriteDatagram(client); advance: // advance for next frame client->framenum++; finish: // clear all unreliable messages still left finish_frame(client); } } static void write_pending_download(client_t *client) { sizebuf_t *buf; size_t remaining; int chunk, percent; if (!client->download) return; if (!client->downloadpending) return; if (client->netchan->reliable_length) return; buf = &client->netchan->message; if (buf->cursize > client->netchan->maxpacketlen) return; remaining = client->netchan->maxpacketlen - buf->cursize; if (remaining <= 4) return; chunk = client->downloadsize - client->downloadcount; if (chunk > remaining - 4) chunk = remaining - 4; client->downloadpending = qfalse; client->downloadcount += chunk; percent = client->downloadcount * 100 / client->downloadsize; SZ_WriteByte(buf, client->downloadcmd); SZ_WriteShort(buf, chunk); SZ_WriteByte(buf, percent); SZ_Write(buf, client->download + client->downloadcount - chunk, chunk); if (client->downloadcount == client->downloadsize) { SV_CloseDownload(client); SV_AlignKeyFrames(client); } } /* ================== SV_SendAsyncPackets If the client is just connecting, it is pointless to wait another 100ms before sending next command and/or reliable acknowledge, send it as soon as client rate limit allows. For spawned clients, this is not used, as we are forced to send svc_frame packets synchronously with game DLL ticks. ================== */ void SV_SendAsyncPackets(void) { qboolean retransmit; client_t *client; netchan_t *netchan; size_t cursize; FOR_EACH_CLIENT(client) { // don't overrun bandwidth if (svs.realtime - client->send_time < client->send_delta) { continue; } netchan = client->netchan; // make sure all fragments are transmitted first if (netchan->fragment_pending) { cursize = netchan->TransmitNextFragment(netchan); SV_DPrintf(0, "%s: frag: %"PRIz"\n", client->name, cursize); goto calctime; } // spawned clients are handled elsewhere if (client->state == cs_spawned && !client->download && !client->nodata && !SV_PAUSED) { continue; } // see if it's time to resend a (possibly dropped) packet retransmit = (com_localTime - netchan->last_sent > 1000); // don't write new reliables if not yet acknowledged if (netchan->reliable_length && !retransmit && client->state != cs_zombie) { continue; } // just update reliable if needed if (netchan->type == NETCHAN_OLD) { write_reliables_old(client, netchan->maxpacketlen); } // now fill up remaining buffer space with download write_pending_download(client); if (netchan->message.cursize || netchan->reliable_ack_pending || netchan->reliable_length || retransmit) { cursize = netchan->Transmit(netchan, 0, NULL, 1); SV_DPrintf(0, "%s: send: %"PRIz"\n", client->name, cursize); calctime: SV_CalcSendTime(client, cursize); } } } void SV_InitClientSend(client_t *newcl) { int i; List_Init(&newcl->msg_free_list); List_Init(&newcl->msg_unreliable_list); List_Init(&newcl->msg_reliable_list); newcl->msg_pool = SV_Malloc(sizeof(message_packet_t) * MSG_POOLSIZE); for (i = 0; i < MSG_POOLSIZE; i++) { List_Append(&newcl->msg_free_list, &newcl->msg_pool[i].entry); } // setup protocol if (newcl->netchan->type == NETCHAN_NEW) { newcl->AddMessage = add_message_new; newcl->WriteDatagram = write_datagram_new; } else { newcl->AddMessage = add_message_old; newcl->WriteDatagram = write_datagram_old; } } void SV_ShutdownClientSend(client_t *client) { free_all_messages(client); Z_Free(client->msg_pool); client->msg_pool = NULL; List_Init(&client->msg_free_list); }