nuclide/Documentation/Networking.md

11 KiB

Networking

Preface

Networking in FTEQW is an extension of the design decisions made for QuakeWorld. All you have to know is that we use NACK feedback to network the changes from client to server. There are a lot of other great resources that document the QuakeWorld protocol, so we will focus on the additions that matter here.

Leveraged by FTE QuakeWorld

In Nuclide, we make heavy use of custom entity networking. In FTE, you can set the entity field .SendEntity and .SendFlags to control network updates for the given entity. You have to implement your own .SendEntity function that communicates:

  1. The type of entity (so the client can interpret it once it arrives)
  2. The type of information that has changed (you'll see references to flChanged in code a lot)
  3. The data that itself has to be networked manually, with fixed and agreed upon size.

Entity updates within Nuclide

To see the type of entity updates we handle internally in Nuclide, look no further than src/shared/entities.h. For one-time event updates, you can check src/shared/events.h.

Generally, those entity updates happen and are controlled by these three common methods that are implemented by any entity class that chooses to override networking:

  • EvaluateEntity
  • SendEntity
  • ReceiveEntity

Let's go over them, one by one.

EvaluateEntity

Called once every frame. This is where we check if anything has changed. We commonly use the macros EVALUATE_FIELD( field, changedFlag ) and EVALUATE_VECTOR( field, xyz, changedFLAG ) to test if any particular field has changed since last frame.

For these macros to work, your entity needs to have declared attributes with the PREDICTED_INT( x ) , PREDICTED_FLOAT( x ), PREDICTED_VECTOR( x ), PREDICTED_BOOL( x ), PREDICTED_ENTITY( x ), or PREDICTED_STRING( x ) macros.

Once the frame ends, we will network the differences in the next method.

SendEntity

As already mentioned in the FTE section, this is where the actual networking gets done.

This is where we have helper macros for dealing with sending and flagging the updates reliably.

  • SENDENTITY_BYTE( field, changedFlag )
  • SENDENTITY_SHORT( field, changedFlag )
  • SENDENTITY_INT( field, changedFlag )
  • SENDENTITY_FLOAT( field, changedFlag )
  • SENDENTITY_STRING( field, changedFlag )
  • SENDENTITY_COORD( field, changedFlag )
  • SENDENTITY_ANGLE( field, changedFlag )
  • SENDENTITY_ENTITY( field, changedFlag )

Most of them are self explanatory, but in the case of SENDENTITY_ANGLE we actually network a short, since we don't need full floating point precision for most types of angles. Keep that in mind if you're running into precision issues.

ReceiveEntity

Method is called on the client-side for each respective entity class we want to handle. First however, we need to talk about the handler.

We allow any game to implement their own handler for entity updates in the function ClientGame_EntityUpdate(float type, bool isNew) which you're encouraged to implement. This is where we check for a handler first, then Nuclide will attempt to handle it instead.

We usually just read the flags field (which we assume is a float for most entities) and then call the ReceiveEntity(float isNew, float flChanged) method, in which we'll use the following macros to help read networked information:

  • READENTITY_BYTE( field, changedFlag )
  • READENTITY_SHORT( field, changedFlag )
  • READENTITY_INT( field, changedFlag )
  • READENTITY_FLOAT( field, changedFlag )
  • READENTITY_STRING( field, changedFlag )
  • READENTITY_COORD( field, changedFlag )
  • READENTITY_ANGLE( field, changedFlag )
  • READENTITY_ENTITY( field, changedFlag )

As you can tell, it's the same setup as in SendEntity - which is by design. This will make keeping fields in check much easier. A simple find-and-replace of the word SEND with READ will do the job most of the time.

We'd like to streamline a lot of this further, so this may be subject to change.

Event updates

The server will use the FTE QuakeWorld supported SVC_CGAMEPACKET packet type to network events.

An example of such an event is as follows, it can be called at any time, anywhere on the server:

	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EV_TEST);
	WriteByte(MSG_MULTICAST, 123); 
	multicast([0,0,0], MULTICAST_ALL_R);

This event will then be sent to ALL clients, reliably as indicated by the MULTICAST_ALL_R. On the client-side, we will handle any event in Event_Parse(float eventType) that you are not handling yourself within ClientGame_EventParse(float eventType).

From there you can use the builtins readbyte(), readshort() etc. which are also used under-the-hood in the macros for entity updates above. Yes, ideally this should be more consistent and nicer to use in the future - but we left it pretty stock in this case.

Client to Server communication

There's a few different means of networking things to the server as a client.

sendevent

sendevent() is the builtin you call from the client game to communicate, reliably, to the server game running on the other end.

The first parameter of sendevent() specifies the function to call in server game:

	sendevent( "Myfunction", "" );

However, it will not look for void() Myfunction on the server game, there is a prefix reserved for all sendevent calls. That one is CSEv. So in reality, the above command will execute this on the server game:

void CSEv_Myfunction ( void ) { }

Now, what about that second parameter?

.. , "" );

Well, sendevent() has the ability to send data. 6 arguments can be passed, max. That second parameter string specifies the type of data you want to send.

For example, you want to ask the server to set the health of a player to a specific value...

Client game:

	sendevent( "SetPlayerHealth", "f", 124 );

Server game:

	void CSEv_SetPlayerHealth_f ( float h ) {
		self.health = h; // naughty! clients can spoof these packets...
	}

As you can see, the f indicates the type float. s would indicate a string. v would indicate a vector. i would indicate an integer. e would indicate an entity (more about that in the last chapter).

The second parameter specifying the arguments will append to the name of the function you're trying to call.

Client game:

	sendevent( "SetColorAndName", "fffs", 255, 0, 128, "Tommy" );

Server game:

	void CSEv_SetColorAndName_fffs ( float r, float g, float b, string name ) {
		self.colormod = [r, g, b];
		self.netname = name;
	}

..is what a longer sendevent with multiple arguments would look like. 6 arguments are the maximum. This is because QuakeC supports 8 arguments max per function call. If you, however, only want to send floats and require more than 6 arguments, you can store them inside vectors:

Client game:

	vector v1 = [ myfloat1, myfloat2, myfloat3 ];
	vector v2 = [ myfloat4, myfloat5, myfloat6 ];
	vector v3 = [ myfloat7, myfloat8, myfloat9 ];
	vector v4 = [ myfloat10, myfloat11, myfloat12 ];
	vector v5 = [ myfloat13, myfloat14, myfloat15 ];
	vector v6 = [ myfloat16, myfloat17, myfloat18 ];
	sendevent( "Bah", "vvvvvv", v1, v2, v3, v4, v5, v6 );

Server game:

	void CSEv_Bah_vvvvvv ( vector v1, vector v2, vector v3, vector v4, vector v5,
	vector v6 ) {
		myfloat1 = v1[0];
		myfloat2 = v1[1];
		myfloat3 = v1[2];
		myfloat4 = v2[0];
		...
	}

Note about sendevent() and entities

If you pass an entity via sendevent, it'll in reality send only the entnum. The entnum is the essentially the entity-id. If the entity does not exist via the server game (like, it has been removed since or is a client-side only entity) then the entity parameter on the server game function call will return world aka 0/NULL.

For protective reasons, entities that are removed have their entnums reserved for a specific amount of time before being able to be used again. Player entities, for example, will allow their entnums to be recycled after 2 seconds. Any other entity its entnum will be reusable after only half a second. This should avoid most entnum conflicts.

Basically, during this time, the parameter will not return world.

Note: This is entirely unreliable behaviour. Check if the .classname is valid on the server before doing anything fancy on it.

clientcommand

Whenever the client issues a cmd based command, say: cmd say foobar in console or via the client game in general, the server will forward it to the active NSGameRules based game rule class within the ClientCommand(NSClient client, string command) method. This is useful for things you do every once in a while.

ServerInfo

ServerInfo keys can be set by server admins, or the game-logic. Those keys are networked to clients inside the game, as well as outside of the game. They can be queried through tools like qstat or GameSpy.

Setting ServerInfo

Console (admins/debugging)

When in the console, you can set the key foo to the value bar like this:

serverinfo foo bar

Server-side code

The same thing can be done in the SSQC side with this line of code:

forceinfokey(world, "foo", "bar");

Retrieving ServerInfo

Console (Debugging)

At any time, the server and clients can enter serverinfo by itself into the console, and it will print out all known ServerInfo keys.

Some of the keys are set by the engine, noteworthy ones include *bspversion and mapname. I encourage you try it and see if there's any keys that seem interesting to you and to make note of them.

Client/server-side code

We have two builtins to query ServerInfo keys from the client-side. serverkey and serverkeyfloat. Their usage goes like this:

string maybeBar = serverkey("foo");
float someNum = serverkeyfloat("maxclients");

UserInfo

UserInfo keys are means of allowing clients (and the server, more on that later) to communicate bits of information about themselves to the server and all other clients. Much like ServerInfo. Those are networked regardless of whether another client is in the same PVS.

Setting UserInfo

Console (clients)

Clients can set their own custom infokey data at any time, using the setinfo console command. For example: setinfo foo bar will set the infokey foo to the value of bar.

Server-side code

The server can also assign its own infokeys to a player. These can be overriden by the client at any given time - unless the server prefixes them with an asterisk. Here is an example: forceinfokey(somePlayer, "*team", "1");

Client infokeys are visible to all other clients. That's how most of the scoreboard is filled in with information. So be careful about the type of data you store in them. Storing passwords or other sensitive information is not recommended.

Retrieving UserInfo

Client-side code

On the client-side, if you wanted to query your own players' specific infokey value, you can query it like this:

float myTeam = getplayerkeyfloat(player_localnum, "*team");

And if you want to query a specific player entity, this is perfectly valid:

string theirName = getplayerkey(playerEnty.entnum-1, "name");

As to why we have to subtract 1 from an entity its entnum for this, is because the clients in the infokey table start at 0, and player entities on the server start at 1; 0 being reserved for world.

Server-side code

The server can then read any given infokey on a given client like so:

string value = infokey(someplayer, "foo");
float otherValue = infokeyf(someplayer, "somenumber");