After several hours of reading, thinking and arguing, Nightcracker and I managed to hammer out a networking system.
The whole thing resembles the
Quake 3 Networking system a lot, which basically works like this:
The server sends a snapshot update at a constant rate, pumping all the data which doesn't have to be reliable, like player x/y, velocities, states (hp) and stuff.
When an event happens, the server immediately sends that event everywhere (asynchronously). It also appends that event to a player-specific event buffer.
In each update to a player, the whole event buffer that hasn't been acked yet gets sent before the snapshot.
When a client receives a packet, it updates a "last_received_packet" variable. This variable is sent in each input update the client sends (which is asynchronous too).
The server, when it gets that ack, deletes all the buffered events that were in that packet, and continues.
Stuff gets even more complicated though, because we want to use delta compression. This means we only send the difference between two packets, which means only sending the XOR of the new packet over the old one.
This means we also have to send the packet which the new packet is based on, and update this with incoming acks. This also means both clients and server have to buffer a certain number of packets.
PyGG2 PROTOCOL CONCEPT
char = 1 byte - ascii, implementation defined
byte = 1 byte - integer, range [-128, 127]
ubyte = 1 byte - integer, range [0, 255]
short = 2 bytes − integer, range [-32768, 32767]
ushort = 2 bytes - integer, range [0, 65535]
int = 4 bytes − integer, range [-2147483648, 2147483647]
uint = 4 bytes - integer, range [0, 4294967295]
long = 8 bytes − integer, range [-9223372036854775808, 9223372036854775807]
ulong = 8 bytes - integer, range [0, 18446744073709551615]
float = 4 bytes - floating point, single precision
double = 8 bytes - floating point, double precision
bin[n] = n bytes - binary data
bit inputstate {
bit left;
bit right;
bit up;
bit mouseleft;
bit mouseright;
}
struct snapshot_player_base {
bin[1] inputstate;
ushort aimdirection;
uint x;
uint y;
short hspeed;
short vspeed;
ubyte hp;
}
All: afterburn, movestatus
Scout: doublejump
Pyro: no afterburn
Soldier: ---
Heavy: Sandvich
Demo: stickies (amount)
Medic: Ubercharge, ubercharge rate, healing ramp
Engie: Nuts&Bolts
Spy: ---
Sniper: Damage(?)
Methods
def send_complete_update {
[ACKED; Server-->Client]
for player in players:
{
send: ServerPlayerJoin(player.name, buffer);
send: ServerPlayerChangeclass(id, player.class, buffer);
send: ServerPlayerChangeteam(id, player.team, buffer);
}
for player in players:
send: stats[KILLS, DEATHS, HEALING, etc...]
if player.character is alive,
{
send: player.input and aimdirection
send: player.positions and velocities
send: player.hp and weapon ammo
send: is cloaked?
send: weapon.readytoshoot
}
if player.sentry exists,
{
send: original pointing direction
send: positions
send: built y/n?
send: hp
}
::Send Gamemode information::
}
Handshake
[ACKED; 2-Way]
> client hello
name; pass if any;
> server hi
if pass = incorrect: notify incorrect pass and kick
if numplayers >= maxplayers: notify server full and kick
send: servername; numOfPlayers; mapName, version
send_complete_update()
Snapshot_update
[NOT_ACKED; Server-->Client]
Send all player positions and speed.
for player in players {
if player is alive,
{
send: player.input and aimdirection
send: player.positions and velocities
send: player.hp and ammo
send: cloaked(y/n)?
{
}
Packet pre- and suffixes:
server -> client
ushort sequence;
ushort delta_sequence;
ushort last_acked_sequence;
client -> server
ushort sequence;
ushort ack_sequence;