Here below is described how the network-protocol works.
The protocol has been designed to be stable, little to no desyncs, and still fast and playable with a dialup modem. We believe we succeeded in this.
OpenTTD mostly uses TCP. It also makes use of UDP, but below you can read more about that.
OpenTTD - TCP
All main communication goes via TCP. Why not UDP, you can ask. Very simple: TCP is a packet-protocol. One packet depends on the other. We are completely sure in TCP that a client or server receives a certain packet. With UDP we are not. Of course you can build a layer on top of the UDP that does this, but then you just remade TCP.
So why do we need a packet-protocol. Let me try to explain:
Everything you build in OpenTTD, or change, is handled internally via a so called DoCommand. Instead of sending diffs of the memory every 10 ms or something to make clients and servers sync, we make use of this DoCommand. At the moment a client does a DoCommand, it does not execute it yet, but the command is sent to the server. The server picks a frame-of-execute for the DoCommand, and sends that information to all clients. When the clients hit that frame, it executes the DoCommand. This way all clients and server keeps in sync. But as you can understand, it is needed that all packets are received, in order, by both server and clients. That is why we make use of a Packet-protocol.
How does it work?
As you have read above, we send DoCommands. Now in normal conditions, the server sends out a FRAME-packet every frame. In this frame it says to what frame the client can 'walk'. Just before that, the server has sent all DoCommands for the max-frame in the FRAME-packet. This way, all clients 'walk' to the max-frame, and then execute all DoCommands. Because a client can never be beyond the max-frame, neither can the server, all DoCommands are executed on the same frame. This is very much needed, else we get massive desyncs. Sometimes it is necessary to increase the freq. of this FRAME-packet (e.g. many slow connections). Then what happens is this: the server sends out a FRAME-packet every X seconds, containing a max-frame of X+freq. Also, all DoCommands have the frame-of-execute set on X+freq. Again, this way we are sure all DoCommands are handled at the same moment (even the same order). What does this mean? Very simple: when you build a rail for example, it can take 'freq' frames before it shows up. So as you understand, this 'freq' can not be too high. Some statistics:
|1||1.2 kbyte/sec||1.2 kbyte/sec|
|2||0.8 kbyte/sec||0.8 kbyte/sec|
As you can see, your connection has to be really slow to adjust the frame_freq. You can change the frame_freq in the console with: '*net_frame_freq = <number>'. Remember the * in front.
What is this desync?
Desync happens when a client is out of sync with the server. How do we detect that? Pretty simple: a game can be different, when DoCommands are not handled, but even worse: when the Random is not the same. OpenTTD has a home-build Randomizer, which means that on every machine it gives the same random numbers from a certain seed. So when seed = 1, and we do a random, we get on every machine, for example, 534523 back. This is very useful. We check once in the sync_freq if the seeds on both clients and server is the same. If this is not the same, it means that the server or one of the clients did a random too much or too little. Because we cannot check which one made an error, this client is then kicked out of the server and can rejoin to resync.
In the old protocol this happened a lot, mostly because of a bad network protocol. Packets were not always sent in order, and clients could walk 100 frames in front of the server and received, on random frames, DoCommands. This was the cause of the desync's 99% of the time. This is fixed now. I can't remember someone complaining about a desync in this new protocol, simply because it is almost impossible to have a different random-seed.
By default, the seeds are checked once in the 100 frames. You can alter this with '*net_sync_freq = <number>'. Do not make this number too low, because it increases bandwidth by a lot.
The network protocol is 100% endian safe. We made that possible by creating our own endian, sort to speak. We do not send packets, but we send bytes over the network. For example, when we want to send a int64, we send first byte 1, then byte 2, and so on. So the byte order is ALWAYS 1 2 3 4. This way we know for sure that every client can join every server, as far as endianness is concerned.
OpenTTD has a function which can detect LAN-servers. This is done via UDP. At the moment you select LAN-mode on the server, the server also starts an UDP-listener. When a client on the same LAN presses Find Server in LAN-mode, it sends an UDP-broadcast. This is one of the best things UDP has: broadcast. The server receives it, and sends his game_info back to the client. This way the client can discover a server on the local LAN. This is the only place where UDP is used.
When a client joins, it first fetches some data from the server (game-info). Then it verifies itself, gives his name and, if needed, passwords. Then it asks for a copy of the current map. We do this, because we need a start. In the map is also saved the random-seeds, and we also send which frame the map is of. So the server makes the copy of the map, starts sending it to the client. At that moment, the server queues all new DoCommands for this client. When the client is done receiving and tells the server that it is also done loading the map, the servers dequeues all the DoCommands, and the client is given some time to get back to the frame the server is currently on (of course time passed between starting receiving and done loading. Even more on slow connections). When the client indicates it is done with the dequeue, the client is in sync and can enjoy playing.
As always, not everything is done. Some things that needs to be done:
- Ban players GUI - MOTD
Below is described which packets are sent (for the real ID of a packet, check network_data.h (enum PacketType)) to or from the server. Also is added which data is expected to be in there.
The client can get the server info without authorization. But before it gets the map, it needs authorization. After it received the map, it starts playing, and also the player is announced to the other clients.
Below first the packets the client can send to the server. After that, the packets the server can send to the client.
- An uintX is X bits that together makes an unsigned something. So an uint8 is just a byte, and uint32 is four bytes in the order 1 2 3 4 that together makes an unsigned int32.
- A string are multiple chars behind eachother. You must read it till you reach a zero. There is no length info attached.
|PACKET_CLIENT_GAME_INFO||Request server-info (first packet, always needed)||
|PACKET_CLIENT_JOIN||Try to join the server||
- String: OpenTTD revision of the client (norev000 if no revision) - String: Playername - Uint8: Playas (between 1 and MAX_PLAYERS) - Uint8: LanguageID
|PACKET_CLIENT_PASSWORD||Send a password to the server to authorize ourself||
- Uint8: Type of password (Game-Password (0) or Company-Password (1)) - String: Password
|PACKET_CLIENT_GETMAP||Request the map from the server||
|PACKET_CLIENT_MAP_OK||Tell the server that we are done receiving/loading the map||
|PACKET_CLIENT_ACK||Tell the server we are done with this frame||
- Uint32: _frame_counter of the client
|PACKET_CLIENT_COMMAND||Send a DoCommand to the Server||
- Uint8: PlayerID (0..MAX_PLAYERS-1) - Uint32: CommandID (see command.h) - Uint32: P1 (free variables used in DoCommand) - Uint32: P2 - Uint32: Tile (tile of DoCommand) - Uint32: decode_params < 10 times the last one (lengthof(cp->dp)) > - Uint8: CallBackID (see callback_table.c)
|PACKET_CLIENT_CHAT||Send a chat-packet to the server||
- Uint8: ActionID (see network_data.h, NetworkAction) - Uint8: Destination Type (see network_data.h, DestType); - Uint8: Destination Player (1..MAX_PLAYERS) - String[MAX_TEXT_MSG_LEN]: Message
|PACKET_CLIENT_ERROR||The client made an error and is quiting the game||
- Uint8: ErrorID (see network_data.h, NetworkErrorCode)
|PACKET_CLIENT_SET_PASSWORD||Set the password for the clients current company||
- String: Password
|PACKET_CLIENT_QUIT||The client is quiting the game||
- String: Leave message
|PACKET_SERVER_CLIENT_INFO||Sends info about a client (you receive this packet during the whole game (whenever someone joins or changes his name)||
- Uint8: The index of the client (always unique on a server. 1 = server) - Uint8: As which player the client is playing - String: The name of the client
|PACKET_SERVER_GAME_INFO||Sends info about the server||
- Uint8: Version of this GAME_INFO packet (current version is 4) From version 4: - Uint8: Number of active newgrfs (let number be NG) - NG x (Int64, Int256): GrfID and MD5sum of active newgrf. NOTE - these have opposite endianness! THey are big-endian numbers. From version 3: - Uint32: Current game date - Uint32: Start date From version 2: - Uint8: Maximum number of companies - Uint8: Current number of companies - Uint8: Maximum number of spectators From version 1: - String: The servername - String: The revision of the server (norev000 if not supported) - Uint8: The LangaugeID of the server - Uint8: If the server is using a password - Uint8: Max allowed clients on the server - Uint8: Current clients - Uint8: Current spectators - Uint16: Current game-date of the server (pre-version 3 only) - Uint16: Start date of the game (pre-version 3 only) - String: Name of the current map (Random is random) - Uint16: Width of the map - Uint16: Height of the map - Uint8: Set of the map (0..3) - Uint8: Set if the server is dedicated
|PACKET_SERVER_ERROR||The client, who receives this message, made an error||
- Uint8: ErrorID (see network_data.h, NetworkErrorCode)
|PACKET_SERVER_NEED_PASSWORD||Indication to the client that the server needs a password||
- Uint8: Type of password (game-password (0) or company-password (1))
|PACKET_SERVER_WELCOME||The client is joined and ready to receive his map||
- Uint8: own ClientID (unique number per server session)
|PACKET_SERVER_WAIT||The client can not receive the map at the moment because someone else is already receiving the map||
- Uint8: Clients awaiting map
|PACKET_SERVER_MAP||Sends the map to the client, or a part of it (it is splitted in a lot of multiple packets)||
- uint8: packet-type (MAP_PACKET_START, MAP_PACKET_NORMAL and MAP_PACKET_END) if MAP_PACKET_START: - uint32: The current FrameCounter if MAP_PACKET_NORMAL: - uint8[SEND_MTU]: piece of the map (till max-size of packet) if MAP_PACKET_END: - uint32: seed0 of player - uint32: seed1 of player < last 2 are repeated MAX_PLAYERS time >
|PACKET_SERVER_JOIN||A client is joined (also the client who just joined receives this message)||
- Uint8: ClientID
|PACKET_SERVER_FRAME||Sends the current frame-counter to the client||
- Uint32: Frame Counter - Uint32: Frame Counter Max (how far may the client walk?) - [Uint32: general-seed-1] - [Uint32: general-seed-2] (last two depends on compile-settings, and are not default settings)
|PACKET_SERVER_SYNC||Sends a sync-check to the client||
- Uint32: Frame Counter - Uint32: General-seed-1 - [Uint32: general-seed-2] (last one depends on compile-settings, and are not default settings)
|PACKET_SERVER_COMMAND||Sends a DoCommand to the client||
- Uint8: PlayerID (0..MAX_PLAYERS-1) - Uint32: CommandID (see command.h) - Uint32: P1 (free variables used in DoCommand) - Uint32: P2 - Uint32: Tile (tile of DoCommand) - Uint32: decode_params < 10 times the last one (lengthof(cp->dp)) > - Uint8: CallBackID (see callback_table.c) - Uint32: Frame of execution
|PACKET_SERVER_CHAT||Sends a chat-packet to the client||
- Uint8: ActionID (see network_data.h, NetworkAction) - Uint8: ClientID - String[MAX_TEXT_MSG_LEN]: Message
|PACKET_SERVER_ERROR_QUIT||One of the clients made an error and is quiting the game. This packet informs the other clients of that.||
- Uint8: ClientID - Uint8: ErrorID (see network_data.h, NetworkErrorCode)
|PACKET_SERVER_QUIT||A client left the game, and this packets informs the other clients of that.||
- Uint8: ClientID - String: Leave message
|PACKET_SERVER_SHUTDOWN||Let the clients know that the server is closing||
|PACKET_SERVER_NEWGAME||Let the clients know that the server is loading a new map||