A networking library for the awesome Löve engine.
This library aims to take care of the enourmous overhead of connecting, authorizing, passwords, synchronizing game states, usernames, userlist etc. involved when creating a network game. There are other networking libraries available, such as LUBE, Noobhub, or you could even use plain Luasocket (which this library uses internally). However, Affair is much more high-level. Because it has many functions which are frequently used in multiplayer games, using Affair should speed up multiplayer game development a lot.
- Callbacks for important events (new user, disconnected etc.) can be defined.
- Automatic synchronizing of player names and player IDs
- Server is independent of Löve and can be run as a dedicated, headless, plain-Lua server (example included).
- Online serverlist
- LAN serverlist (UDP-Broadcast)
- Automatically synched user values. Simply call client:setUserValue( "red", 255 ) and let the library handle synchronization. All other clients will now have access to this user's "red" value, and it will be "255" on all clients - it will even be available on clients who join after the setUserValue has been called. This way, any info about users (character, stats, position, upgrades, hitpoints etc.) are handled by the engine without you needing to worry about them.
- Calculates and stores ping time of players.
- Automatic handling of usernames. If a name appears multiple times, the library automatically appends numbers and increments them.
The lib comes with an example (main.lua). Run a server using:
love . --server
Connect a user by calling:
love . --client ADDRESS
ADDRESS is the IP address. Defaults to 'localhost'.
Default port is 3410.
You can also create a server and connect a client to it at the same time:
love . --server --client localhost
Another included example is the dedicated server, which runs in plain Lua (Luasocket must be installed. If you have Löve installed, then this is usually the case.) Run:
lua examples/dedicated.lua
Then connect a client to it by running the client example above.
A server is started using the network:startServer function:
server, err = network:startServer( numberOfPlayers, port, pingUpdate, portUDP )
- numberOfPlayers is the maximum number of clients that may connect to the server (default 16).
- port is the port number (Servers will need to port-forward this port if they're behind a router. Use a value between 1025 and 65535, default is 3410)
- pingUpdate specifies how often the server should ping clients. (To check for timeouts, for example. Value in seconds, default is 5 seconds.)
- portUDP sets the port number to be used for creating a LAN server list. Use a value between 1025 and 65535, default is 3410) Upon success, this function returns a server object which can be used to control the server (send stuff, kick clients, close connections, get list of clients, get number of clients etc.) If the function fails, it returns nil and an err will be filled with the error message. Most of the time, port, pingUpdate and portUDP can be left at their defaults, so you would call the function like so:
-- creates a server which allows a maximum of 8 connections.
server, err = network:startServer( 8 )
if server then
...
else
print("Could not start server: " .. err )
end
Once you have created a server object, you can define the server's callbacks. If, for example, "authorize" and "serverReceive" are functions with the correct parameters, then you can define the callbacks like this:
server, err = network:startServer( ... )
if server then
server.callbacks.received = serverReceive
server.callbacks.authorize = authorize
end
server.callbacks.received( command, msg, user ): Called whenever the server receives a message from a user (which is not an engine-internal message). So whenever you call client:send( command, msg ) on a client, this event will fire on the server.
server.callbacks.userFullyConnected( user ): Called when a user has connected AND has been synchronized. "user" is the newly connected user, which has a player name and id set already. Ideally, you should never interact with a user before this callback has fired. Important: before this callback has fired, any broadcasts will not be forwarded to this user.
server.callbacks.synchronize( user ): This callback is called during the connection process of a new user. If there are vital objects/information which the client needs before joining the game (for example, the current map or the other clients' player entities) then it should be sent to the client here. Note: At this point, the new client knows about all other clients, so it's okay to send client-specific data - like the player entities - which might require knowledge about the other players. Note: At this point, the new client also knows the current status of all of the other users' customData (userValues) which have previously been set. Note: If you use server:send(...) in this function to send values to the new user, make sure to give the third parameter to the function (the "user" value). Otherwise, server:send broadcasts this info to all synchronized clients - and the others usually already have the data. Note: Do not user server:setUserCallback here (it will throw an error), because the user must be fully synchronized before setUserValue works. If you need to set custom user data, use server:setUserCallback in the userFullyConnected
server.callbacks.authorize( user, authMsg ): Called when a new user is trying to connect. Use this event to let the engine know whether or not a new user may connect at the moment. This event should return either true or false followed by an error message. If this event is not specified, it Example usage: The authorize event could return true while the server is in a lobby, but as soon as the actual game is started, it returns: false, "Game already started!". The client will then be disconnected and userFullyConnected and synchronize (above) will never be called for this client. Note: You don't need to worry about the maximum number of players here - if the server is already full, then the engine will not authorize the player and won't even call this event. authMsg is the string which the client used when calling network:startClient. This way, you can check if the client is using the same game version as you, or entered the correct password.
server.callbacks.customDataChanged( user, value, key, previousValue ): Called whenever a client changes their customUserData. The userdata is already synched with other clients, but if you want to do something when user data changes (example: start game when sets his "ready" value to true), then this is the place. The previousValue is the value which the user had set before - it could be nil, if this value is set for the first time.
server.callbacks.disconnectedUser( user ): Called when a user has disconnected. Note: after this call, the "user" table will be invalid. Don't attempt to use it again - but you're allowed to access it to print the user name of the client who left and similar:
function disconnected( user )
print( user.playername .. " has has left. (ID: " .. user.id .. ")" )
end
A client is started (and connected to an already running server) by calling network:startClient.
client, err = network:startClient( address, playername, port, authMsg )
- address: The IP v4 Address to connect to (example: "192.168.0.10", default: "localhost").
- playername: The player name to use as the client. This may be changed by the server if a player with the same name already exists.
- port: The port the server is running on. Make sure this is the same as the server's port setting! (default: 3410)
- authMsg: The authorization message which the server will use to check if the client may connect. This can be a version string or a password (or both, just concatenate them). The message will be sent to the server where the server.callbacks.authorize function will be called (if set). The server can then use the authMsg string to determine whether this client will be allowed to connect or not. The call returns a client object if successful (which can be used to send data, set user values, and disconnect the client again) or nil followed by an error message.
Once you have created a client object, you can define the client's callbacks. If, for example, "connect" and "clientReceive" are functions with the correct parameters, then you can define the callbacks like this:
client, err = network:startClient( ... )
if client then
client.callbacks.connected = connect
client.callbacks.received = clientReceive
end
client.callbacks.authorized( auth, reason ): This is called when the server responds to the authorization request by the client (which the client will always to automatically when connecting). The 'auth' paramter will be true or false depending on whether the client has been authorized. The "reason" parameter will hold a message in case the client has not been authorized, telling it, why.
client.callbacks.connected(): Called on the client when the connection process has finished (similar to the server.callbacks.userFullyConnected callback called on the server) and the client is synchronized. At this point, the client is 'equal' to all other clients who have previously connected and has their user values, names and IDs.
client.callbacks.received( command, msg ): Called when the client gets a message from the server (i.e. when server:send( command, msg ) has been called on the server.
client.callbacks.disconnected(): Called when the client has been disconnected for some reason.
client.callbacks.newUser( user ): Called on all clients when a new user has been synchronized. Note: This is not called on the client who is joining (i.e. the one who has just been synchronized). Note: You do not need to keep a list of all users. Use client:getUsers() to get an up-to-date list of all currently connected users.
Maximum size of a message is 4 GB (to be more precise: 256^4 bytes). You should always stay well below, of course - but hey, I won't stop you.
For the online server list to work, you will need to install some sort of web-server (a simple Apache server will do, you need php support) and put the files from the AffairMainServer into some folder on that main server (The only requirement is that the folder is visible to anyone). On your game's server, call the following function (after having created a server using network:startServer):
server:advertise( data, id, url, portUDP )
- data: A string with any info about the server which you want the clients to get, before joining. This may contain a name, a password, the number of players, the map name etc.
- id: A name which identifies the game
- the URL to the folder where the scripts are on your main server. For example, if you have the advertise.php in a path like this: ~/web/public/AffairMainServer/advertise.php, then the URL given here should be ~/web/public/AffairMainServer.
- portUDP: The UDP Port which will be used for the LAN server (default: 3410). Important: If you change this, you must also pass the same port to network:requestServerListLAN() (see below). If in doubt, do not use this parameter.
Careful! The data and id strings may not contain the following characters: " & $ | and any whitespace (space, tab, newline). Usually, you're best off by using a comma-seperated list.
The function will start sending updates about your server every minute or so, to tell the web server that your server is still alive, and will stop sending updates when your server goes offline.
You can change the server's data, by calling the function again with only the data parameter - for example when you want to change the map or the number of players.
If you want to stop advertising the server, call (for example because a round has started and you want no more players to join): server:unAdvertise()
Example:
function startServer()
-- Attempt to create a server:
server, err = network:startServer( NUMBER_OF_PLAYERS, PORT, PING_UPDATE_TIME )
if server then
-- These callbacks are called when a new user connects or a user disconnected:
server.callbacks.userFullyConnected = connected
server.callbacks.disconnectedUser = disconnected
-- Start advertising this server, so others can join:
server:advertise( "Players:0", "ExampleServer", MAIN_SERVER_ADDRESS )
else
print("Error starting server:", err)
love.event.quit()
end
end
-- Update number of players on the serverlist:
function connected()
local players = network:getUsers()
-- Only update the data field in the advertisement, leave the id and URL the same:
server:advertise( "Players:" .. #players )
end
function disconnected()
local players = network:getUsers()
-- Only update the data field in the advertisement, leave the id and URL the same:
server:advertise( "Players:" .. #players - 1 )
end
When calling the server:advertise function above, the server will also advertise itself in your Local Area Network. This uses a UDP port to accept messages from clients. The port can be set as the fourth argument in network:startServer (see above).
On the client, you can start looking for LAN servers using:
network:requestServerListLAN( id, portUDP )
- id must be the same name as given to the server:advertise call on the server. The game filters out any servers which don't have the same ID, so make sure this is set correctly
- portUDP is an optional port you can set so that the UDP broadcast works on this port. Important: If you change this on the client, you must also change it on the server, when calling server:advertise! If in doubt, do not use this parameter.
You may call this function again to refresh the LAN server list (i.e. send out a new request). In this case, you can call it without parameters - the previous ones will be used.