About ===== The R1Q2 protocol is a modified version of the "protocol 34" used by Quake II 3.20. It's primary goal is to save bandwidth by making best use of available bit space and applying compression where savings can be made. It also aims to extend functionality in a few ways, the most notable at present being the ability to receive large (fragmented) UDP packets, effectively increasing the amount of action that can happen at once. Please note that this document only contains the changes that are required to implement client-side protocol 35 support. The server side support is significantly more complex. If you are interested, check out the R1Q2 source code from http://www.r1ch.net/stuff/r1q2/. I advise against modifying the protocol 35 implementation without changing the protocol number. Also note that since recording demos essentially just dumps packets from the network, protocol 35 demos will be unplayable on clients that do not support protocol 35. Due to the evolving nature of the protocol, old demos may also become incompatible when new changes are introduced to the protocol at a later date. It is recommended to use protocol 34 if stable demo recording is required, or to translate all the protocol 35 messages into a protocol 34 compatible form when writing demos (this won't be easy!) List of changes in no particular order ====================================== The Quake Port (qport) is changed from 2 bytes to 1 byte. The qport serves to track client(s) behind a broken NAT router that will randomly change the source port of sent packets. Since it is unlikely more than 256 users will be playing from a single IP, the 2 byte qport was effectively a waste of space. In the unlikely event that two users are behind a broken NAT router and the randomly selected qports are identical, it should be made easy for a user to manually set the qport via a cvar to some fixed value. For clients who don't have broken routers, it should also be possible to send the qport "0" in the "connect" message, indicating to the server that the qport will not be present which allows for additional bandwidth savings. Compressed packet type (svc_zpacket, value 21). This is a new server message that contains a zlib-packed packet. For all use of zlib, the windowBits should be set to -15 to avoid trying to read/write an unneeded zlib header. The svc_zpacket structure is two 16 bit integers followed by a stream of compressed data. The first integer specifies the compressed length. The second integer specifies the uncompressed length. Using the first integer, you should read the remainder of the message and decompress it into a buffer of appropriate size. The decompressed message should then be parsed as if it were itself a packet of server messages. The optimum way to do this would be to temporarily alter the "net_message" variable and call CL_ParseServerMessage, taking care to restore the old contents after the function returns. Another new compressed packet type exists, svc_zdownload (value 22). This indicates a compressed download packet. The format is of a standard download packet, with an additional 16 bit integer prior to the data indicating the uncompressed length. The data should then be decompressed and used as if it were a standard download packet. Server messages themselves (ie the command byte - svc_frame, svc_muzzleflash, etc) never exceed the value 32. This provides 3 extra bits that are otherwise unused. The new protocol makes use of these bits for additional information that can be passed to a function. Currently, only svc_frame makes use of these bits. The first 5 bits of the command byte indicate the message type, the remaining 3 are custom data that are message -specific. Example code to parse in CL_ParseServerMessage: cmd = MSG_ReadByte (&net_message); if (cmd == -1) { SHOWNET("END OF MESSAGE"); break; } extrabits = cmd & 0xE0; cmd &= 0x1F; The svc_frame message itself is altered to provide additional bandwidth savings. The 32 bit integer containing the serverframe now also contains the offset of the deltaframe encoded in the last 5 bits, instead of requring an entirely new 32 bit integer. The special offset of 31 indicates a delta of -1 (ie none). An example piece of code to parse the new serverframe format: serverframe = MSG_ReadLong (&net_message); offset = serverframe & 0xF8000000; offset >>= 27; serverframe &= 0x07FFFFFF; cl.frame.serverframe = serverframe; if (offset == 31) cl.frame.deltaframe = -1; else cl.frame.deltaframe = serverframe - offset; Additionally, the surpressCount byte has been altered. Since surpressCount will never be more than 9, using a full byte results in a lot of unused bits. These bits are now used as additional playerstate delta flags in conjunction with extra bits from the message type (discussed above). The first 4 bits of surpressCount indicate the surpressCount, the last 4 bits indicate extra flags for the playerstate. The extraflags variable must also be initialized from the extrabits supplied by CL_ParseServerMessage. Example code: extraflags = extrabits >> 1; data = MSG_ReadByte (&net_message); cl.surpressCount = (data & 0x0F); extraflags |= (data & 0xF0) >> 4; The svc_playerinfo and svc_packetentities bytes have been removed from the svc_frame message as they serve no purpose and are simply static bytes that waste bandwidth. When parsing the playerstate from CL_ParseFrame, pass the extraflags parameter to allow the new delta flags to be used. The following additional values are delta'd separately in the new protocol playerstate: origin[2], velocity[2], viewangles[2], gunoffset, gunangles, statbits. These provide savings of up to 16 bytes per playerstate delta depending on conditions. The following modified code shows how to parse the new delta flags. "enhanced" is a variable indicating whether or not the client is connected using the new protocol in order to allow for backwards compatibility. if (flags & PS_M_ORIGIN) { if (!enhanced) extraflags |= EPS_PMOVE_ORIGIN2; state->pmove.origin[0] = MSG_ReadShort (&net_message); state->pmove.origin[1] = MSG_ReadShort (&net_message); } if (extraflags & EPS_PMOVE_ORIGIN2) state->pmove.origin[2] = MSG_ReadShort (&net_message); if (flags & PS_M_VELOCITY) { if (!enhanced) extraflags |= EPS_PMOVE_VELOCITY2; state->pmove.velocity[0] = MSG_ReadShort (&net_message); state->pmove.velocity[1] = MSG_ReadShort (&net_message); } if (extraflags & EPS_PMOVE_VELOCITY2) state->pmove.velocity[2] = MSG_ReadShort (&net_message); ... if (flags & PS_VIEWANGLES) { if (!enhanced) extraflags |= EPS_VIEWANGLE2; state->viewangles[0] = MSG_ReadAngle16 (&net_message); state->viewangles[1] = MSG_ReadAngle16 (&net_message); } if (extraflags & EPS_VIEWANGLE2) state->viewangles[2] = MSG_ReadAngle16 (&net_message); ... if (flags & PS_WEAPONFRAME) { if (!enhanced) extraflags |= EPS_GUNOFFSET|EPS_GUNANGLES; state->gunframe = MSG_ReadByte (&net_message); } if (extraflags & EPS_GUNOFFSET) { state->gunoffset[0] = MSG_ReadChar (&net_message)*0.25f; state->gunoffset[1] = MSG_ReadChar (&net_message)*0.25f; state->gunoffset[2] = MSG_ReadChar (&net_message)*0.25f; } if (extraflags & EPS_GUNANGLES) { state->gunangles[0] = MSG_ReadChar (&net_message)*0.25f; state->gunangles[1] = MSG_ReadChar (&net_message)*0.25f; state->gunangles[2] = MSG_ReadChar (&net_message)*0.25f; } ... if (!enhanced) extraflags |= EPS_STATS; // parse stats if (extraflags & EPS_STATS) { statbits = MSG_ReadLong (&net_message); if (statbits) { for (i=0 ; istats[i] = MSG_ReadShort(&net_message); } } The new flags are defined as follows: #define EPS_GUNOFFSET (1<<0) #define EPS_GUNANGLES (1<<1) #define EPS_PMOVE_VELOCITY2 (1<<2) #define EPS_PMOVE_ORIGIN2 (1<<3) #define EPS_VIEWANGLE2 (1<<4) #define EPS_STATS (1<<5) CL_ParseDelta has a minor change in the new protocol. In protocol 34, the old_origin for RF_BEAM entities is constantly sent (ie not delta'd) which is a waste of bandwidth. Protocol 35 provides a delta for this entity type as follows: if (!enhanced) VectorCopy (from->origin, to->old_origin); else if (!(bits & U_OLDORIGIN) && !(from->renderfx & RF_BEAM)) VectorCopy (from->origin, to->old_origin); The svc_serverdata message is changed to include some new protocol 35 specific information. After the level name string comes a single byte, 1 or 0, to indicate whether the server is in enhanced Game DLL mode. This should be ignored at present. Following this byte is a 16 bit integer specifying the current protocol 35 revision. At present, this is the value "1901". The high value is required so that older protocol 35 clients that did not support reading this message will fail upon reading the serverdata and fall back to protocol 34 until the client has updated to the new protocol 35 revision. If the server sends a different revision number to what the client was compiled with, the client should fall back to protocol 34. The maximum packet size a client can receive is increased to 4096 bytes to allow for larger amounts of packetentities and other data. A simple way to support this is to simply increase the MAX_MSGLEN definition. Be careful as this will also affect the server and may cause problems for protocol 34 clients if care is not taken to ensure the server uses the traditional 1400 byte limit. The client "connect" message is updated to allow the client to choose how large a packet they want to receive. Since some consumer routers are unable to correctly reassemble fragmented UDP, the value should ideally default to 1390. It is sent as the 5th argument in the connectionless "connect" message as an integer, for example: Netchan_OutOfBandPrint (NS_CLIENT, &adr, "connect %i %i %i \"%s\" %u\n", cls.serverProtocol, port, cls.challenge, Cvar_Userinfo(), maxMessageLength); A value of 0 indicates to use the maximum possible length. One final change in protocol 35 is the free-flying observer movement. This is doubled to allow spectators a faster way to move around the map. To apply this change, simply check if a player is in the PM_SPECTATOR state. If so, simply double the pml.frametime in the Pmove function. The recommended way to do this is to extend the pmove_t structure to specify a multiplier. Since Pmove is a shared function and called by the Game DLL, be sure to create a wrapper function such as SV_Pmove and provide that to the Game API to avoid incompatibility. Questions? Pop onto irc.edgeirc.net #qdev. Last updated: 2005-04-01.