├── game.lua ├── menu.lua ├── commands.lua ├── serverlist ├── unAdvertiseOnline.lua ├── requestOnlineThreadded.lua ├── advertiseOnline.lua └── advertiseOnlineThreadded.lua ├── user.lua ├── License.txt ├── utility.lua ├── network.lua ├── examples └── dedicated.lua ├── client.lua ├── main.lua ├── advertise.lua ├── server.lua └── README.md /game.lua: -------------------------------------------------------------------------------- 1 | local game = {} 2 | 3 | function game:init() 4 | end 5 | 6 | function game:update( dt ) 7 | end 8 | 9 | function game:draw() 10 | end 11 | 12 | function game:keypressed( key ) 13 | end 14 | 15 | function game:mousepressed( button, x, y ) 16 | end 17 | 18 | return game 19 | -------------------------------------------------------------------------------- /menu.lua: -------------------------------------------------------------------------------- 1 | local menu = {} 2 | 3 | function menu:init() 4 | local scr = ui.newScreen( "Lobby" ) 5 | ui.setActiveScreen( "Lobby" ) 6 | 7 | scr:addPanel( "menu panel" 8 | end 9 | 10 | function menu:update( dt ) 11 | end 12 | 13 | function menu:draw() 14 | end 15 | 16 | function menu:keypressed( key ) 17 | end 18 | 19 | function menu:mousepressed( button, x, y ) 20 | end 21 | 22 | return menu 23 | -------------------------------------------------------------------------------- /commands.lua: -------------------------------------------------------------------------------- 1 | 2 | -- List of all possible internal commands. 3 | -- EVERY message is lead by a command byte from to following list. 4 | -- If it's not on this list then it's considered a user command and will be 5 | -- send to the client:receive and server:receive callbacks. 6 | local CMD = 7 | { 8 | -- Connection process: 9 | PLAYERNAME = 1, 10 | PLAYER_AUTHORIZED = 2, 11 | NEW_PLAYER = 3, 12 | AUTHORIZED = 4, 13 | 14 | -- Other 15 | USER_VALUE = 5, 16 | PLAYER_LEFT = 6, 17 | 18 | KICKED = 7, 19 | 20 | AUTHORIZATION_REQUREST = 8, 21 | 22 | PING = 9, 23 | PONG = 11, 24 | USER_PINGTIME = 12, 25 | } 26 | 27 | return CMD 28 | -------------------------------------------------------------------------------- /serverlist/unAdvertiseOnline.lua: -------------------------------------------------------------------------------- 1 | -- This is part of the "Affair" library. 2 | -- This file handles sending a server's data to the main server list. 3 | -- Luasockets must be installed for this to work. If you have Löve installed, this is already the case. 4 | 5 | local http = require("socket.http") 6 | 7 | print( "[UNADVERTISE] Attempting to connect" ) 8 | 9 | local URL = arg[1] or "" 10 | local PORT = arg[2] or "" 11 | 12 | print( "[ADVERTISE] Contacting: " .. URL ) 13 | 14 | local body = "" 15 | body = body .. "port=" .. PORT.. "&" 16 | 17 | local result, errCode, errorMsg, status = http.request( URL, body ) 18 | if errCode and errCode ~= 200 then 19 | print("[ADVERTISE] Could not un-advertise: " .. errCode, status, "Correct URL?", URL ) 20 | end 21 | 22 | -- Close this process: 23 | os.exit() 24 | -------------------------------------------------------------------------------- /user.lua: -------------------------------------------------------------------------------- 1 | local User = {} 2 | User.__index = User 3 | 4 | function User:new( connection, playerName, id ) 5 | local o = {} 6 | setmetatable( o, self ) 7 | o.connection = connection 8 | o.incoming = { 9 | part = "", -- store partly received messages here 10 | length = nil -- store length of incoming message here 11 | } 12 | 13 | o.playerName = playerName 14 | o.id = id 15 | o.authorized = false 16 | o.synchronized = false 17 | 18 | o.ping = { 19 | timer = 0, 20 | waitingForPong = false, 21 | pingReturnTime = 0, 22 | } 23 | 24 | o.customData = {} 25 | 26 | return o 27 | end 28 | 29 | function User:setPlayerName( name ) 30 | self.playerName = name 31 | self.receivedPlayername = true 32 | end 33 | 34 | function User:getPing() 35 | return self.ping.pingReturnTime 36 | end 37 | 38 | return User 39 | -------------------------------------------------------------------------------- /serverlist/requestOnlineThreadded.lua: -------------------------------------------------------------------------------- 1 | -- This script is run in a seperate Löve thread to make sure the main script is not blocked: 2 | -- It will get a list of internet servers from a "main server (given by the URL)", if possible. 3 | local http = require("socket.http") 4 | 5 | local arg = {...} 6 | 7 | -- The channel to use when printing out server files: 8 | local cout = arg[1] 9 | 10 | -- The url: 11 | local URL = arg[2] 12 | 13 | -- The game name/id: 14 | local ID = arg[3] 15 | 16 | local body = "" 17 | body = body .. "id=" .. ID .. "&" 18 | 19 | --cout:push(body) 20 | 21 | local result, errCode, errMsg, status = http.request( URL .. "/getList.php", body ) 22 | 23 | if errCode and errCode >= 400 then 24 | cout:push( errCode .. " '" .. (status or "Unknown error" ) .. "' Is URL correct? " .. URL .. "/getList.php" ) 25 | end 26 | 27 | -- If successful, send back the lines one by one: 28 | for line in result:gmatch("([^\n]*)\n")do 29 | if #line > 0 then 30 | cout:push( "[Entry] " .. line) 31 | end 32 | end 33 | 34 | cout:push("End") 35 | return 36 | -------------------------------------------------------------------------------- /serverlist/advertiseOnline.lua: -------------------------------------------------------------------------------- 1 | -- This is part of the "Affair" library. 2 | -- This file handles sending a server's data to the main server list. 3 | -- Luasockets must be installed for this to work. If you have Löve installed, this is already the case. 4 | 5 | local http = require("socket.http") 6 | 7 | local URL = arg[1] or "" 8 | local PORT = arg[2] or "" 9 | local ID = arg[3] or "" 10 | local INFO = arg[4] or "" 11 | 12 | --print( "[ADVERTISE] Contacting: " .. URL ) 13 | 14 | local body = "" 15 | body = body .. "port=" .. PORT.. "&" 16 | body = body .. "id=" .. ID .. "&" 17 | body = body .. "info=" .. INFO .. "&" 18 | 19 | local result, errCode, errorMsg, status = http.request( URL, body ) 20 | 21 | local err = result:match( "%[Warning:%]%s?(.-)\n" ) 22 | if err then 23 | print( "[ADVERTISE] " .. err ) 24 | --else 25 | -- print( "[ADVERTISE] Advertisement sent:", PORT, ID, INFO ) 26 | end 27 | 28 | if errCode and errCode >= 400 then 29 | print( "[ADVERTISE] Could not advertise: " .. errCode, status, "Correct URL?", URL ) 30 | end 31 | 32 | -- Close this process: 33 | os.exit() 34 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Micha Pfeiffer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /utility.lua: -------------------------------------------------------------------------------- 1 | local BASE = (...):match("(.-)[^%.]+$") 2 | 3 | local utility = {} 4 | 5 | local b1 = 256^3 6 | local b2 = 256^2 7 | local b3 = 256 8 | 9 | function utility:lengthToHeader( len ) 10 | -- Short message: 11 | if len < 255 then 12 | return string.char(len) 13 | end 14 | 15 | -- Long message ('255' followed by 4 bytes of length) 16 | local h1 = math.floor(len/b1) 17 | len = len - h1*b1 18 | local h2 = math.floor(len/b2) 19 | len = len - h2*b2 20 | local h3 = math.floor(len/b3) 21 | len = len - h3*b3 22 | --print("\t",255, h1, h2, h3, len) 23 | return string.char(255,h1,h2,h3,len) 24 | end 25 | 26 | function utility:headerToLength( header ) 27 | local byte1 = string.byte( header:sub(1,1) ) 28 | if byte1 <= 254 then 29 | return byte1, 1 30 | else 31 | if #header == 5 then 32 | local v1 = string.byte(header:sub(2,2))*b1 33 | local v2 = string.byte(header:sub(3,3))*b2 34 | local v3 = string.byte(header:sub(4,4))*b3 35 | local v4 = string.byte(header:sub(5,5)) 36 | return v1 + v2 + v3 + v4, 5 37 | end 38 | end 39 | -- If the length is larger than 254, but no 5 bytes have arrived yet... 40 | return nil 41 | end 42 | 43 | return utility 44 | -------------------------------------------------------------------------------- /serverlist/advertiseOnlineThreadded.lua: -------------------------------------------------------------------------------- 1 | -- This is part of the "Affair" library. 2 | -- This file handles sending a server's data to clients via UDP, should they request it. 3 | -- Luasockets must be installed for this to work. If you have Löve installed, this is already the case. 4 | 5 | local http = require("socket.http") 6 | 7 | arg = {...} 8 | local cin = arg[1] 9 | local cout = arg[2] 10 | 11 | while true do 12 | local msg = cin:demand() 13 | local command, content = msg:match( "(.-)|(.*)" ) 14 | if command == "PORT" then 15 | PORT = content 16 | elseif command == "ID" then 17 | ID = content 18 | elseif command == "INFO" then 19 | INFO = content 20 | elseif command == "URL" then 21 | URL = content 22 | elseif command == "advertise" then 23 | local body = "" 24 | body = body .. "port=" .. PORT.. "&" 25 | body = body .. "id=" .. ID .. "&" 26 | body = body .. "info=" .. INFO .. "&" 27 | local result, errCode, errorMsg, status = http.request( URL .. "/advertise.php", body ) 28 | local err = result:match( "%[Warning:%](.-)\n" ) 29 | if err then 30 | cout:push( "Warning:" .. err) 31 | cout:push("closed") 32 | return 33 | elseif errCode and errCode >= 400 then -- don't send two warnings 34 | local msg = "Warning: Could not advertise: \"" .. tostring(status) .. "\"" 35 | if errCode == 404 then 36 | msg = msg .. "\n\tWrong URL? (" .. URL .. "/advertise.php)" 37 | end 38 | cout:push( msg ) 39 | cout:push("closed") 40 | return 41 | end 42 | elseif command == "unAdvertise" then 43 | local body = "" 44 | body = body .. "port=" .. PORT.. "&" 45 | local result, errCode, errorMsg, status = http.request( URL .. "/unAdvertise.php", body ) 46 | if errCode and errCode >= 400 then -- don't send two warnings 47 | local msg = "Warning: Could not unAdvertise: \"" .. tostring(status) .. "\"" 48 | if errCode == 404 then 49 | msg = msg .. "\n\tWrong URL? (" .. URL .. "/advertise.php)" 50 | end 51 | cout:push( msg ) 52 | end 53 | cout:push("closed") 54 | return 55 | elseif command == "close" then 56 | cout:push("closed") 57 | return 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /network.lua: -------------------------------------------------------------------------------- 1 | local BASE = (...):match("(.-)[^%.]+$") 2 | local BASE_SLASH = BASE:sub(1,#BASE-1) .. "/" 3 | 4 | local Server = require( BASE .. "server" ) 5 | local Client = require( BASE .. "client" ) 6 | 7 | -- Load advertising (serverlist) submodule 8 | local advertise = require( BASE .. "advertise" ) 9 | 10 | local network = {} 11 | 12 | network.advertise = advertise 13 | 14 | local conn = nil 15 | local connectionType = "" 16 | local connected = false 17 | 18 | local users = {} 19 | 20 | local PORT = 3410 -- port used to send data (TCP) 21 | 22 | local server = nil 23 | local client = nil 24 | 25 | function network:startServer( maxNumberOfPlayers, port, pingTime ) 26 | local createServer = function() 27 | return Server:new( maxNumberOfPlayers, port or PORT, pingTime, UDP_BROADCAST_PORT ) 28 | end 29 | 30 | success, server = pcall( createServer ) 31 | local err = "" 32 | if not success then 33 | err = server 34 | server = nil 35 | end 36 | return server, err 37 | end 38 | 39 | function network:startClient( address, playername, port, authMsg ) 40 | 41 | if not address or #address == 0 then 42 | print("[NET] No address found. Using default: 'localhost'") 43 | address = "localhost" 44 | end 45 | 46 | print( "[NET] Connecting to:", address, port, authMsg) 47 | 48 | local createClient = function() 49 | return Client:new( address, port or PORT, playername, authMsg ) 50 | end 51 | 52 | success, client = pcall( createClient ) 53 | local err = "" 54 | if not success then 55 | err = client 56 | client = nil 57 | end 58 | assert(client, "Could not connect." ) 59 | return client, err 60 | end 61 | 62 | function network:closeConnection() 63 | print("[NET] Closing all connections.") 64 | if client then 65 | client:close() 66 | end 67 | if server then 68 | server:close() 69 | end 70 | end 71 | 72 | function network:update( dt ) 73 | if server then 74 | -- If updating the server returns false, then 75 | -- the connection has been closed. 76 | if not server:update( dt ) then 77 | server = nil 78 | end 79 | end 80 | if client then 81 | -- If updating the client returns false, then 82 | -- the connection has been closed. 83 | if not client:update( dt ) then 84 | client = nil 85 | end 86 | end 87 | 88 | advertise:update( dt ) 89 | end 90 | 91 | function network:getUsers() 92 | if server then 93 | return server:getUsers(), server:getNumUsers() 94 | end 95 | if client then 96 | return client:getUsers(), client:getNumUsers() 97 | end 98 | end 99 | 100 | --[[function network:send( command, msg ) 101 | if client then 102 | client:send( command, msg ) 103 | end 104 | end]] 105 | 106 | function stringToType( value, goalType ) 107 | if goalType == "number" then 108 | return tonumber(value) 109 | elseif goalType == "boolean" then 110 | return value == "true" and true or false 111 | end 112 | -- if it was meant to be a string, return it as such: 113 | return value 114 | end 115 | 116 | return network 117 | -------------------------------------------------------------------------------- /examples/dedicated.lua: -------------------------------------------------------------------------------- 1 | -- This is a minimal example of how to set up a dedicated server. 2 | -- In this context, dedicated means the server is run 3 | -- a) headless (no Löve dependency) 4 | -- b) in plain Lua. 5 | -- Requirements: Luasocket and Lua must be installed 6 | 7 | network = require( "../network" ) 8 | local server 9 | local MAX_PLAYERS = 16 10 | local PORT = 3412 11 | 12 | local MAIN_SERVER_ADDRESS = "http://germanunkol.de/Affair/advertise.php" 13 | 14 | -- COMMANDs are used to identify messages. 15 | -- Custom commands MUST be numbers between (including) 128 and 255. 16 | -- Make sure these are the same on client and server. 17 | -- Ideally, put them into a seperate file and include it from both client 18 | -- and server. Here, I leave it in the main file for readability. 19 | local COMMAND = { 20 | CHAT = 128, 21 | MAP = 129, 22 | } 23 | 24 | local myMapString = "" 25 | 26 | function startDedicatedServer() 27 | local success 28 | success, server = pcall( function() 29 | return network:startServer( MAX_PLAYERS, PORT ) 30 | end) 31 | 32 | if success then 33 | -- set callbacks for the newly created server: 34 | setServerCallbacks( server ) 35 | 36 | network.advertise:setURL( MAIN_SERVER_ADDRESS ) 37 | network.advertise:setID( "ExampleServer" ) 38 | network.advertise:setInfo( "Players:0" ) 39 | network.advertise:start( server, "both" ) 40 | else 41 | -- If I can't start a server for some reason, let user know and exit: 42 | print(server) 43 | os.exit() 44 | end 45 | end 46 | 47 | function connected( user ) 48 | -- Called when new user has fully connected. 49 | print( user.playerName .. " has joined. (ID: " .. user.id .. ")" ) 50 | 51 | local list, num = network:getUsers() 52 | network.advertise:setInfo( "Players:" .. num ) 53 | end 54 | function disconnected( user ) 55 | -- Called when user leaves. 56 | print( user.playerName .. " has has left. (ID: " .. user.id .. ")" ) 57 | 58 | local list, num = network:getUsers() 59 | network.advertise:setInfo( "Players:" .. num ) 60 | end 61 | function synchronize( user ) 62 | -- Send the map to the new client 63 | server:send( COMMAND.MAP, myMapString, user ) 64 | print("sent map") 65 | end 66 | function authorize( user ) 67 | -- Authorize everyone! We're a lövely community, after all, everyone is welcome! 68 | return true 69 | end 70 | function received( command, msg, user ) 71 | -- If the user sends us some data, then just print it. 72 | print( user.playerName .. " sent: ", command, msg ) 73 | 74 | -- NOTE: Usually, you would compare "command" to all the values in the COMMAND table, and 75 | -- then act accordingly: 76 | -- if command == COMMAND. 77 | end 78 | 79 | function setServerCallbacks( server ) 80 | 81 | -- Called whenever one of the users is trying to connect: 82 | server.callbacks.authorize = authorized 83 | 84 | -- Called during connection process: 85 | server.callbacks.synchronize = synchronize 86 | 87 | -- Called when user has connected AND has been synchronized: 88 | server.callbacks.userFullyConnected = connected 89 | 90 | -- Called whenever one of the users sends data: 91 | server.callbacks.received = received 92 | 93 | -- Called whenever one of the users is disconnected 94 | server.callbacks.disconnectedUser = disconnected 95 | end 96 | 97 | -- Sleep time in second - use the socket library to make sure the 98 | -- sleep is a "non busy" sleep, meaning the CPU will NOT be busy during 99 | -- the sleep. 100 | function sleep( sec ) 101 | socket.select(nil, nil, sec) 102 | end 103 | 104 | -- Fill the map string with something long for testing purposes: 105 | for y = 1, 900 do 106 | for x = 1, 90 do 107 | if math.random(10) == 1 then 108 | myMapString = myMapString .. math.random(9) 109 | else 110 | myMapString = myMapString .. "-" 111 | end 112 | end 113 | myMapString = myMapString .. "\n" 114 | if y % 1000 == 0 then 115 | print(y) 116 | end 117 | end 118 | print( "Map:\n" .. myMapString .. "\nNumber of characters: " .. #myMapString ) 119 | 120 | startDedicatedServer() 121 | 122 | local time = socket.gettime() 123 | local dt = 0 124 | local t = 0 125 | while true do 126 | network:update( dt ) 127 | 128 | dt = socket.gettime() - time 129 | time = socket.gettime() 130 | 131 | -- This is important. Play with this value to fit your need. 132 | -- If you don't use this sleep command, the CPU will be used as much as possible, you'll probably run the game loop WAY more often than on the clients (who also require time to render the picture - something you don't need) 133 | sleep( 0.05 ) 134 | end 135 | -------------------------------------------------------------------------------- /client.lua: -------------------------------------------------------------------------------- 1 | 2 | local BASE = (...):match("(.-)[^%.]+$") 3 | 4 | local socket = require("socket") 5 | 6 | local User = require( BASE .. "user" ) 7 | local CMD = require( BASE .. "commands" ) 8 | 9 | local utility = require( BASE .. "utility" ) 10 | 11 | local Client = {} 12 | Client.__index = Client 13 | 14 | local userList = {} 15 | local numberOfUsers = 0 16 | 17 | local partMessage = "" 18 | local messageLength = nil 19 | 20 | function Client:new( address, port, playerName, authMsg ) 21 | local o = {} 22 | setmetatable( o, self ) 23 | 24 | authMsg = authMsg or "" 25 | 26 | print("[NET] Initialising Client...") 27 | o.conn = socket.tcp() 28 | o.conn:settimeout(5) 29 | local ok, msg = o.conn:connect( address, port ) 30 | --ok, o.conn = pcall(o.conn.connect, o.conn, address, port) 31 | if ok and o.conn then 32 | o.conn:settimeout(0) 33 | self.send( o, CMD.AUTHORIZATION_REQUREST, authMsg ) 34 | print("[NET] -> Client connected", o.conn) 35 | else 36 | o.conn = nil 37 | return nil 38 | end 39 | 40 | o.callbacks = { 41 | authorized = nil, 42 | received = nil, 43 | connected = nil, 44 | disconnected = nil, 45 | otherUserConnected = nil, 46 | otherUserDisconnected = nil, 47 | customDataChanged = nil, 48 | } 49 | 50 | userList = {} 51 | partMessage = "" 52 | 53 | o.clientID = nil 54 | o.playerName = playerName 55 | 56 | numberOfUsers = 0 57 | 58 | -- Filled if user is kicked: 59 | o.kickMsg = "" 60 | 61 | return o 62 | end 63 | 64 | function Client:update( dt ) 65 | if self.conn then 66 | local data, msg, partOfLine = self.conn:receive( 9999 ) 67 | if data then 68 | partMessage = partMessage .. data 69 | else 70 | if msg == "timeout" then 71 | if #partOfLine > 0 then 72 | partMessage = partMessage .. partOfLine 73 | end 74 | elseif msg == "closed" then 75 | --self.conn:shutdown() 76 | print("[NET] Disconnected.") 77 | if self.callbacks.disconnected then 78 | self.callbacks.disconnected( self.kickMsg ) 79 | end 80 | self.conn = nil 81 | return false 82 | else 83 | print("[NET] Err Received:", msg, data) 84 | end 85 | end 86 | 87 | if not messageLength then 88 | if #partMessage >= 1 then 89 | local headerLength = nil 90 | messageLength, headerLength = utility:headerToLength( partMessage:sub(1,5) ) 91 | if messageLength and headerLength then 92 | partMessage = partMessage:sub(headerLength + 1, #partMessage ) 93 | end 94 | end 95 | end 96 | 97 | -- if I already know how long the message should be: 98 | if messageLength then 99 | if #partMessage >= messageLength then 100 | -- Get actual message: 101 | local currentMsg = partMessage:sub(1, messageLength) 102 | 103 | -- Remember rest of already received messages: 104 | partMessage = partMessage:sub( messageLength + 1, #partMessage ) 105 | 106 | command, content = string.match( currentMsg, "(.)(.*)") 107 | command = string.byte( command ) 108 | 109 | self:received( command, content ) 110 | messageLength = nil 111 | end 112 | end 113 | 114 | 115 | --[[if data then 116 | if #partMessage > 0 then 117 | data = partMessage .. data 118 | partMessage = "" 119 | end 120 | 121 | -- First letter stands for the command: 122 | command, content = string.match(data, "(.)(.*)") 123 | command = string.byte( command ) 124 | 125 | self:received( command, content ) 126 | else 127 | if msg == "timeout" then -- only part of the message could be received 128 | if #partOfLine > 0 then 129 | partMessage = partMessage .. partOfLine 130 | end 131 | elseif msg == "closed" then 132 | --self.conn:shutdown() 133 | print("[NET] Disconnected.") 134 | if self.callbacks.disconnected then 135 | self.callbacks.disconnected( self.kickMsg ) 136 | end 137 | self.conn = nil 138 | return false 139 | else 140 | print("[NET] Err Received:", msg, data) 141 | end 142 | end]] 143 | return true 144 | else 145 | return false 146 | end 147 | end 148 | 149 | function Client:received( command, msg ) 150 | if command == CMD.PING then 151 | -- Respond to ping: 152 | self:send( CMD.PONG, "" ) 153 | elseif command == CMD.USER_PINGTIME then 154 | local id, ping = msg:match("(.-)|(.*)") 155 | id = tonumber(id) 156 | if userList[id] then 157 | userList[id].ping.pingReturnTime = tonumber(ping) 158 | end 159 | elseif command == CMD.NEW_PLAYER then 160 | local id, playerName = string.match( msg, "(.*)|(.*)" ) 161 | id = tonumber(id) 162 | local user = User:new( nil, playerName, id ) 163 | userList[id] = user 164 | numberOfUsers = numberOfUsers + 1 165 | if self.callbacks.newUser then 166 | self.callbacks.newUser( user ) 167 | end 168 | elseif command == CMD.PLAYER_LEFT then 169 | local id = tonumber(msg) 170 | local u = userList[id] 171 | userList[id] = nil 172 | numberOfUsers = numberOfUsers - 1 173 | if self.callbacks.otherUserDisconnected then 174 | self.callbacks.otherUserDisconnected( u ) 175 | end 176 | elseif command == CMD.AUTHORIZED then 177 | local authed, reason = string.match( msg, "(.*)|(.*)" ) 178 | if authed == "true" then 179 | self.authorized = true 180 | print( "[NET] Connection authorized by server." ) 181 | -- When authorized, send player name: 182 | self:send( CMD.PLAYERNAME, self.playerName ) 183 | else 184 | print( "[NET] Not authorized to join server. Reason: " .. reason ) 185 | end 186 | 187 | if self.callbacks.authorized then 188 | self.callbacks.authorized( self.authorized, reason ) 189 | end 190 | 191 | elseif command == CMD.PLAYERNAME then 192 | local id, playerName = string.match( msg, "(.*)|(.*)" ) 193 | self.playerName = playerName 194 | self.clientID = tonumber(id) 195 | -- At this point I am fully connected! 196 | if self.callbacks.connected then 197 | self.callbacks.connected() 198 | end 199 | --self.conn:settimeout(5) 200 | elseif command == CMD.USER_VALUE then 201 | local id, keyType, key, valueType, value = string.match( msg, "(.*)|(.*)|(.*)|(.*)|(.*)" ) 202 | 203 | key = stringToType( key, keyType ) 204 | value = stringToType( value, valueType ) 205 | 206 | id = tonumber( id ) 207 | 208 | userList[id].customData[key] = value 209 | 210 | if self.callbacks.customDataChanged then 211 | self.callback.customDataChanged( user, value, key ) 212 | end 213 | elseif command == CMD.KICKED then 214 | 215 | self.kickMsg = msg 216 | print("[NET] Kicked from server: " .. msg ) 217 | 218 | elseif self.callbacks.received then 219 | self.callbacks.received( command, msg ) 220 | end 221 | end 222 | 223 | function Client:send( command, msg ) 224 | 225 | local fullMsg = string.char(command) .. (msg or "") --.. "\n" 226 | 227 | local len = #fullMsg 228 | assert( len < 256^4, "Length of packet must not be larger than 4GB" ) 229 | 230 | fullMsg = utility:lengthToHeader( len ) .. fullMsg 231 | 232 | local result, err, num = self.conn:send( fullMsg ) 233 | while err == "timeout" do 234 | fullMsg = fullMsg:sub( num+1, #fullMsg ) 235 | result, err, num = self.conn:send( fullMsg ) 236 | end 237 | 238 | return 239 | end 240 | 241 | function Client:getUsers() 242 | return userList 243 | end 244 | function Client:getNumUsers() 245 | return numberOfUsers 246 | end 247 | 248 | function Client:close() 249 | if self.conn then 250 | --self.conn:shutdown() 251 | self.conn:close() 252 | print( "[NET] Closed.") 253 | end 254 | end 255 | 256 | function Client:setUserValue( key, value ) 257 | local keyType = type( key ) 258 | local valueType = type( value ) 259 | self:send( CMD.USER_VALUE, keyType .. "|" .. tostring(key) .. 260 | "|" .. valueType .. "|" .. tostring(value) ) 261 | end 262 | 263 | function Client:getID() 264 | return self.clientID 265 | end 266 | 267 | function Client:getUserValue( key ) 268 | if not self.clientID then return nil end 269 | local u = userList[self.clientID] 270 | if u then 271 | return u.customData[key] 272 | end 273 | return nil 274 | end 275 | 276 | function Client:getUserPing( id ) 277 | if users[id] then 278 | return users[id].ping.pingReturnTime 279 | end 280 | end 281 | 282 | return Client 283 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | 2 | network = require( "network" ) 3 | 4 | -- COMMANDs are used to identify messages. 5 | -- Custom commands MUST be numbers between (including) 128 and 255. 6 | -- Make sure these are the same on client and server. 7 | -- Ideally, put them into a seperate file and include it from both client 8 | -- and server. Here, I leave it in the main file for readability. 9 | local COMMAND = { 10 | CHAT = 128, 11 | MAP = 129, 12 | } 13 | 14 | local server = nil 15 | local client = nil 16 | 17 | local NUMBER_OF_PLAYERS = 16 -- Server should not allow more than 16 connections 18 | local PORT = 3412 -- The port which might need to be forwarded 19 | local ADDRESS = "localhost" -- Fallback address to connect client to 20 | 21 | local MAIN_SERVER_ADDRESS = "http://germanunkol.de/Affair/" 22 | 23 | local buttons = {} 24 | 25 | local TITLE = "Love Affair" 26 | local ERROR_MSG = "" 27 | local ERROR_TIMER = 0 28 | 29 | function love.load( args ) 30 | 31 | local startingClient = false 32 | local startingServer = false 33 | local addressFound = false 34 | local serverlist = false 35 | for k, v in ipairs( args ) do 36 | if v == "--client" then 37 | startingClient = true 38 | if args[k+1] then 39 | addressFound = true 40 | ADDRESS = args[k+1] 41 | else 42 | serverlist = true 43 | end 44 | end 45 | if v == "--server" then 46 | startingServer = true 47 | end 48 | if v == "--list" then 49 | serverlist = true 50 | end 51 | if v == "--help" then 52 | printHelp() 53 | love.event.quit() 54 | return 55 | end 56 | end 57 | 58 | if not startingServer and not startingClient and not serverlist then 59 | printHelp() 60 | love.event.quit() 61 | return 62 | end 63 | 64 | if startingServer then 65 | addressFound = true 66 | ADDRESS = 'localhost' 67 | startServer() 68 | end 69 | if startingClient and addressFound then 70 | startClient() 71 | end 72 | 73 | if serverlist and not addressFound then 74 | TITLE = "Server List (F5 to refresh)" 75 | requestServerLists() 76 | end 77 | end 78 | 79 | function requestServerLists() 80 | -- reset any already loaded lists: 81 | buttons = {} 82 | 83 | -- Set callbacks for remote server list: 84 | network.advertise.callbacks.newEntryOnline = newEntryOnline 85 | network.advertise.callbacks.fetchedAllOnline = finishedServerlistOnline 86 | 87 | -- Set callback for LAN server list: 88 | network.advertise.callbacks.newEntryLAN = newEntryLAN 89 | 90 | 91 | -- Request server lists ("ExampleServer is the name of the game used on the server 92 | -- for advertising): 93 | 94 | -- Set meta info needed by request: 95 | network.advertise:setURL( MAIN_SERVER_ADDRESS ) 96 | network.advertise:setID( "ExampleServer" ) 97 | -- Start receiving from LAN and online server lists: 98 | network.advertise:request( "both" ) 99 | end 100 | 101 | function love.quit() 102 | print("Closing") 103 | if client then 104 | client:close() 105 | end 106 | if server then 107 | server:close() 108 | end 109 | end 110 | 111 | function printHelp() 112 | print("Usage:\n\tStart Server:\n\t\tlove . --server\n\n\tStart Client:\n\t\tlove . --client [ADDRESS]\n\t\t(Omit ADDRESS to load list of servers)\n") 113 | end 114 | 115 | function love.update( dt ) 116 | network:update( dt ) 117 | ERROR_TIMER = ERROR_TIMER - dt 118 | end 119 | 120 | function startServer() 121 | server, err = network:startServer( NUMBER_OF_PLAYERS, PORT ) 122 | 123 | if server then 124 | setServerCallbacks() 125 | 126 | network.advertise:setURL( MAIN_SERVER_ADDRESS ) 127 | network.advertise:setID( "ExampleServer" ) 128 | network.advertise:setInfo( "Players:0" ) 129 | network.advertise:start( server, "both" ) 130 | 131 | TITLE = "Server @ Port " .. PORT 132 | else 133 | print("Error starting server:", err) 134 | love.event.quit() 135 | end 136 | 137 | end 138 | 139 | function startClient() 140 | client, err = network:startClient( ADDRESS, "Anonymous", PORT ) 141 | 142 | if client then 143 | setClientCallbacks() 144 | if server then 145 | TITLE = "Server + Client: Connected to " .. ADDRESS .. ", Port: " .. PORT 146 | else 147 | TITLE = "Client: Connected to " .. ADDRESS .. ": Port: " .. PORT 148 | end 149 | else 150 | print("Error connecting client:", err) 151 | love.event.quit() 152 | end 153 | end 154 | 155 | function setServerCallbacks() 156 | server.callbacks.userFullyConnected = connected 157 | server.callbacks.disconnectedUser = disconnected 158 | network.advertise.callbacks.advertiseWarnings = advertiseWarnings 159 | end 160 | 161 | function setClientCallbacks() 162 | 163 | end 164 | 165 | function connected() 166 | local players,numPlayers = network:getUsers() 167 | -- Only update the data field in the advertisement, leave the id and URL the same: 168 | server:advertise( "Players:" .. numPlayers ) 169 | print("Connected.", players, numPlayers) 170 | end 171 | function disconnected() 172 | local players,numPlayers = network:getUsers() 173 | -- Only update the data field in the advertisement, leave the id and URL the same: 174 | server:advertise( "Players:" .. #players - 1 ) 175 | end 176 | 177 | 178 | function newEntryOnline( entry ) 179 | print("Server found at:\n" .. 180 | "\tAddress: " .. entry.address .. "\n" .. 181 | "\tPort: " .. entry.port .. "\n" .. 182 | "\tInfo: " .. entry.info) 183 | 184 | local list = network.advertise:getServerList( "online" ) 185 | 186 | -- Create new button: 187 | local b = { 188 | x = 50, y = 62 + 20*(#list - 1), 189 | w = love.graphics.getWidth() - 100, 190 | h = 18, 191 | text = entry.address .. "\t" .. entry.port .. "\t" .. entry.info, 192 | event = function() chooseServer( entry ) end 193 | } 194 | table.insert( buttons, b ) 195 | end 196 | 197 | function newEntryLAN( entry ) 198 | print("Server found at (LAN):\n" .. 199 | "\tAddress: " .. entry.address .. "\n" .. 200 | "\tPort: " .. entry.port .. "\n" .. 201 | "\tInfo: " .. entry.info) 202 | 203 | local list = network.advertise:getServerList( "lan" ) 204 | 205 | -- Create new button: 206 | local b = { 207 | x = 50, y = love.graphics.getHeight()/2 + 52 + 20*(#list - 1), 208 | w = love.graphics.getWidth() - 100, 209 | h = 18, 210 | text = entry.address .. "\t" .. entry.port .. "\t" .. entry.info, 211 | event = function() chooseServer( entry ) end 212 | } 213 | table.insert( buttons, b ) 214 | end 215 | 216 | 217 | function chooseServer( serverEntry ) 218 | for k, v in pairs(serverEntry) do 219 | print(k,v) 220 | end 221 | ADDRESS = serverEntry.address 222 | PORT = serverEntry.port 223 | startClient() 224 | if client then -- success? 225 | buttons = {} 226 | end 227 | end 228 | 229 | function finishedServerlistOnline( list ) 230 | print("Finished retreiving servers. Servers found:", #list ) 231 | end 232 | 233 | function advertiseWarnings( err ) 234 | ERROR_MSG = err 235 | ERROR_TIMER = 10 236 | end 237 | 238 | function drawServerList() 239 | love.graphics.setColor( 255,255,255,50 ) 240 | love.graphics.rectangle( "fill", 50, 40, love.graphics.getWidth() - 100, 20 ) 241 | love.graphics.setColor( 255,255,255,255 ) 242 | love.graphics.print( "Servers:", 55, 43 ) 243 | 244 | love.graphics.setColor( 255,255,255,50 ) 245 | love.graphics.rectangle( "fill", 50, love.graphics.getHeight()/2 + 30, 246 | love.graphics.getWidth() - 100, 20 ) 247 | love.graphics.setColor( 255,255,255,255 ) 248 | love.graphics.print( "Servers (LAN):", 55, love.graphics.getHeight()/2 + 33 ) 249 | 250 | for k, b in pairs(buttons) do 251 | love.graphics.setColor( 255,255,255,25 ) 252 | love.graphics.rectangle( "fill", b.x, b.y, b.w, b.h ) 253 | love.graphics.setColor( 255,255,255,255 ) 254 | love.graphics.print( b.text, b.x + 5, b.y + 3 ) 255 | end 256 | end 257 | 258 | function drawPlayerList() 259 | 260 | love.graphics.setColor( 255,255,255,50 ) 261 | love.graphics.rectangle( "fill", 50, 40, 250, 18 ) 262 | love.graphics.setColor( 255,255,255,255 ) 263 | love.graphics.print( "Players:", 55, 43 ) 264 | 265 | local players = network:getUsers() 266 | 267 | if players then 268 | local y = 60 269 | for k, p in pairs( players ) do 270 | love.graphics.setColor( 255,255,255,25 ) 271 | love.graphics.rectangle( "fill", 50, y, 250, 18 ) 272 | love.graphics.setColor( 255,255,255,255 ) 273 | love.graphics.print( p.id .. " " .. p.playerName .. " [" .. p.ping.pingReturnTime .. "ms]", 55, y + 3 ) 274 | y = y + 20 275 | end 276 | end 277 | end 278 | 279 | function drawTitle() 280 | love.graphics.setColor( 255,128,64,100 ) 281 | love.graphics.rectangle( "fill", 3, 3, love.graphics.getWidth()-6, 25 ) 282 | love.graphics.setColor( 255,255,255,255 ) 283 | love.graphics.printf( TITLE, 3, 10, love.graphics.getWidth()-6, "center" ) 284 | 285 | if ERROR_TIMER > 0 then 286 | love.graphics.setColor( 255,64,32,100 ) 287 | love.graphics.rectangle( "fill", 3, love.graphics.getHeight() - 44, 288 | love.graphics.getWidth()-6, 40 ) 289 | love.graphics.setColor( 255,255,255,255 ) 290 | love.graphics.printf( ERROR_MSG, 3, love.graphics.getHeight() - 38, 291 | love.graphics.getWidth()-6, "center" ) 292 | end 293 | end 294 | 295 | function love.draw() 296 | if not client and not server then 297 | drawServerList() 298 | elseif client or server then 299 | drawPlayerList() 300 | end 301 | 302 | drawTitle() 303 | end 304 | 305 | function love.mousepressed( x, y, button ) 306 | if not client and not server then 307 | for k, b in pairs( buttons ) do 308 | if b.x < x and b.y < y and b.x+b.w > x and b.y+b.h > y then 309 | b.event() 310 | end 311 | end 312 | end 313 | end 314 | 315 | function love.keypressed( key ) 316 | if key == "f5" then 317 | if not client and not server then 318 | requestServerLists() 319 | end 320 | end 321 | end 322 | -------------------------------------------------------------------------------- /advertise.lua: -------------------------------------------------------------------------------- 1 | -- A submodule used for telling clients about a server. 2 | 3 | -- Get the path to this script: 4 | local BASE = (...):match("(.-)[^%.]+$") 5 | local BASE_SLASH = BASE:sub(1,#BASE-1) .. "/" 6 | 7 | local advertise = { 8 | portUDP = 3410, 9 | advertiseOnlineTimer = 0 10 | } 11 | 12 | advertise.callbacks = { 13 | newEntryOnline = nil, 14 | newEntryLAN = nil, 15 | fetchedAllOnline = nil, -- when done 16 | 17 | advertiseWarnings = nil, -- on error 18 | requestWarnings = nil, -- on error 19 | } 20 | 21 | local ADVERTISEMENT_UPDATE_TIME = 60 -- update every 60 seconds. 22 | 23 | local advertiseOnlineThread = nil 24 | local advertiseOnlineCin = nil 25 | local advertiseOnlineCout = nil 26 | 27 | local requestOnlineThread = nil 28 | local requestOnlineCout = nil 29 | 30 | -- A list containing the servers as retreived from the web server: 31 | local listOnline = {} 32 | -- A list containing servers in the local area network: 33 | local listLAN = {} 34 | 35 | function advertise:setURL( url ) 36 | self.url = url 37 | end 38 | 39 | function advertise:setPortUDP( port ) 40 | assert( port >= 1024 and port <= 65535, 41 | "Port given to setPortUDP must be between 1024 and 65535." ) 42 | self.portUDP = port 43 | end 44 | 45 | function advertise:setID( name ) 46 | -- Only these characters are allowed: 47 | name = name:gsub("[^a-zA-Z0-9%.,:;/%-%+_%%%(%)%[%]!%?']", "") 48 | 49 | self.ID = name 50 | end 51 | 52 | function advertise:setInfo( data ) 53 | -- Only these characters are allowed: 54 | data = data:gsub("[^a-zA-Z0-9%.,:;/%-%+_%%%(%)%[%]!%?']", "") 55 | 56 | self.serverInfo = data 57 | 58 | if self.advertiseOnline then 59 | self.advertiseOnlineTimer = 0 60 | end 61 | end 62 | 63 | function advertise:start( server, where ) 64 | 65 | assert( self.ID, 66 | "Give the application an ID using advertise:setID before calling advertise:start!" ) 67 | assert( server, 68 | "You must pass a valid server to advertise:start!" ) 69 | 70 | if string.lower(where) == "lan" then 71 | self.advertiseLAN = true 72 | self.advertiseOnline = false 73 | elseif string.lower(where) == "online" then 74 | self.advertiseLAN = false 75 | self.advertiseOnline = true 76 | elseif string.lower(where) == "both" then 77 | self.advertiseLAN = true 78 | self.advertiseOnline = true 79 | end 80 | 81 | self.port = server.port 82 | 83 | if self.advertiseLAN then 84 | assert( self.portUDP, 85 | "Give a Port using advertise:setPortUDP before calling advertise:start" ) 86 | 87 | self.advertiseUDP = socket.udp() 88 | self.advertiseUDP:settimeout(0) 89 | self.advertiseUDP:setsockname('*', self.portUDP) 90 | self.advertiseUDP:setoption("broadcast", true) 91 | print("[ADVERTISE] Advertising server in LAN. UDP Port: " .. self.portUDP) 92 | end 93 | 94 | if self.advertiseOnline then 95 | assert( self.url, "Give a URL using advertise:setURL before calling advertise:start" ) 96 | self.advertiseOnlineTimer = 0 97 | 98 | -- If this is run in Löve, rtart a thread to handle the 99 | -- Otherwise, updates will be started in another process (see sendUpdateOnline) 100 | if love then 101 | if advertiseOnlineThread then 102 | advertiseOnlineCin:push("close|") 103 | advertiseOnlineThread = nil 104 | end 105 | 106 | advertiseOnlineThread = love.thread.newThread( 107 | BASE_SLASH .. "serverlist/advertiseOnlineThreadded.lua" ) 108 | advertiseOnlineCin = love.thread.newChannel() 109 | advertiseOnlineCout = love.thread.newChannel() 110 | 111 | advertiseOnlineThread:start( advertiseOnlineCin, advertiseOnlineCout ) 112 | end 113 | print("[ADVERTISE] Advertising server online.") 114 | end 115 | end 116 | 117 | function advertise:stop() 118 | 119 | if self.advertiseLAN or self.advertiseOnline then 120 | print("[ADVERTISE] Stopped advertising the server.") 121 | end 122 | 123 | if self.advertiseLAN then 124 | -- Stop advertising in LAN: 125 | if self.advertiseUDP then 126 | self.advertiseUDP:close() 127 | self.advertiseUDP = nil 128 | end 129 | self.advertiseLAN = false 130 | end 131 | 132 | if self.advertiseOnline then 133 | -- Stop online: 134 | if love and advertiseOnlineThread then 135 | -- Un-Advertise the server: 136 | advertise:sendUpdateOnline( true ) 137 | end 138 | self.advertiseOnline = false 139 | end 140 | end 141 | 142 | function advertise:request( where ) 143 | assert( self.ID, 144 | "Give the application an ID using advertise:setID before calling advertise:start!" ) 145 | 146 | if string.lower(where) == "lan" then 147 | self.requestLAN = true 148 | self.requestOnline = false 149 | elseif string.lower(where) == "online" then 150 | self.requestLAN = false 151 | self.requestOnline = true 152 | elseif string.lower(where) == "both" then 153 | self.requestLAN = true 154 | self.requestOnline = true 155 | end 156 | 157 | if self.requestLAN then 158 | listLAN = {} 159 | assert( self.portUDP, 160 | "Give a Port using advertise:setPortUDP before calling advertise:requestLAN" ) 161 | 162 | self.requestUDP = socket.udp() 163 | self.requestUDP:settimeout(0) 164 | self.requestUDP:setoption('broadcast',true) 165 | self.requestUDP:sendto( "ServerlistRequest|" .. self.ID .. "\n", 166 | "255.255.255.255", self.portUDP) 167 | print( "[REQUEST] Requested LAN servers. UDP Port: " .. self.portUDP ) 168 | end 169 | 170 | if self.requestOnline then 171 | listOnline = {} 172 | assert( self.url, 173 | "Give a URL using advertise:setURL before calling advertise:requestOnline" ) 174 | 175 | assert( love, 176 | "Requesting an online server list only works in Love." ) 177 | 178 | requestOnlineThread = love.thread.newThread( 179 | BASE_SLASH .. "serverlist/requestOnlineThreadded.lua" ) 180 | requestOnlineCout = love.thread.newChannel() 181 | 182 | requestOnlineThread:start( requestOnlineCout, self.url, self.ID ) 183 | print( "[REQUEST] Requested online servers." ) 184 | end 185 | end 186 | 187 | function advertise:stopRequesting() 188 | if self.requestLAN then 189 | self.requestLAN = false 190 | if self.requestUDP then 191 | self.requestUDP:close() 192 | self.requestUDP = nil 193 | end 194 | end 195 | 196 | if self.requestOnline then 197 | self.requestOnline = false 198 | end 199 | 200 | if requestOnlineThread then 201 | requestOnlineThread = nil 202 | end 203 | end 204 | 205 | function advertise:update( dt ) 206 | if self.advertiseLAN then 207 | if self.advertiseUDP then 208 | local data, ip, port = self.advertiseUDP:receivefrom() 209 | if data then 210 | local id = data:match("ServerlistRequest|(.-)\n?$") 211 | if id and id == self.ID then 212 | self.advertiseUDP:sendto( "ServerlistReply|" .. self.ID .. "|" .. self.port .. 213 | "|" .. self.serverInfo .. "\n", ip, port ) 214 | print("[ADVERTISE] Received LAN request. Game ID matched. Answered request.") 215 | end 216 | end 217 | end 218 | end 219 | 220 | if self.advertiseOnline then 221 | if self.advertiseOnlineTimer <= 0 then 222 | self:sendUpdateOnline() 223 | self.advertiseOnlineTimer = ADVERTISEMENT_UPDATE_TIME 224 | end 225 | self.advertiseOnlineTimer = self.advertiseOnlineTimer - dt 226 | end 227 | 228 | if advertiseOnlineThread then 229 | local msg = advertiseOnlineCout:pop() 230 | if msg then 231 | if msg ~= "closed" then 232 | print("[ADVERTISE] " .. msg) 233 | if self.callbacks.advertiseWarnings then 234 | self.callbacks.advertiseWarnings( msg ) 235 | end 236 | else 237 | advertiseOnlineThread = nil 238 | end 239 | end 240 | if advertiseOnlineThread then 241 | local err = advertiseOnlineThread:getError() 242 | if err then 243 | print("[ADVERTISE] " .. err) 244 | advertiseOnlineThread = nil 245 | end 246 | end 247 | end 248 | 249 | if self.requestLAN then 250 | if self.requestUDP then 251 | local data, ip, p = self.requestUDP:receivefrom() 252 | if data then 253 | advertise:parseLANServerEntry( data, ip ) 254 | end 255 | end 256 | end 257 | 258 | if self.requestOnline then 259 | if requestOnlineThread then 260 | -- Check for errors: 261 | local err = requestOnlineThread:getError() 262 | if err then 263 | print("THREAD ERROR: " .. err) 264 | requestOnlineThread = nil 265 | if self.callbacks.requestWarnings then 266 | self.callbacks.requestWarnings( err ) 267 | end 268 | end 269 | -- Get any new messages: 270 | local msg = requestOnlineCout:pop() 271 | if msg then 272 | if not self:parseOnlineServerEntry( msg ) then 273 | requestOnlineThread = nil 274 | if self.callbacks.requestWarnings then 275 | self.callbacks.requestWarnings( msg ) 276 | end 277 | end 278 | end 279 | end 280 | end 281 | end 282 | 283 | function advertise:sendUpdateOnline( unAdvertise ) 284 | if not unAdvertise then 285 | -- advertise the server, i.e. put it onto the server list. 286 | if love then 287 | if advertiseOnlineThread then 288 | -- If thread exists, channels also exist: 289 | advertiseOnlineCin:push( "PORT|" .. self.port ) 290 | advertiseOnlineCin:push( "ID|" .. self.ID ) 291 | advertiseOnlineCin:push( "INFO|" .. self.serverInfo ) 292 | advertiseOnlineCin:push( "URL|" .. self.url ) 293 | advertiseOnlineCin:push( "advertise|" ) 294 | end 295 | 296 | else 297 | os.execute( "lua " .. BASE_SLASH .. "serverlist/advertiseOnline.lua " 298 | .. self.url .. "/advertise.php " 299 | .. self.port .. " " 300 | .. "\"" .. self.ID .. "\" " 301 | .. "\"" .. self.serverInfo .. "\" &" ) 302 | end 303 | else 304 | 305 | -- unAdvertise the server, i.e. remove it from the server list: 306 | if love then 307 | if advertiseOnlineThread then 308 | -- If thread exists, channels also exist: 309 | advertiseOnlineCin:push( "PORT|" .. self.port ) 310 | advertiseOnlineCin:push( "URL|" .. self.url ) 311 | advertiseOnlineCin:push( "unAdvertise|" ) 312 | advertiseOnlineCin:push( "close" ) 313 | end 314 | else 315 | os.execute( "lua " .. BASE_SLASH .. "serverlist/unAdvertiseOnline.lua " 316 | .. self.url .. "/advertise.php " 317 | .. self.port .. " " 318 | .. "\"" .. self.ID .. "\" " 319 | .. "\"" .. self.serverInfo .. "\" &" ) 320 | end 321 | end 322 | end 323 | 324 | function advertise:parseLANServerEntry( data, ip ) 325 | local command, id, port, info = data:match("(.-)|(.-)|(.-)|(.-)\n?$") 326 | if command and id and info then 327 | if command == "ServerlistReply" and id == self.ID then 328 | local e = { 329 | address = ip, 330 | port = port, 331 | info = info, 332 | } 333 | table.insert( listLAN, e ) 334 | if self.callbacks.newEntryLAN then 335 | self.callbacks.newEntryLAN( e ) 336 | end 337 | end 338 | end 339 | end 340 | 341 | function advertise:parseOnlineServerEntry( msg ) 342 | if msg == "End" then 343 | requestOnlineThread = nil 344 | if self.callbacks.fetchedAllOnline then 345 | self.callbacks.fetchedAllOnline( listOnline ) 346 | end 347 | return true 348 | else 349 | local address, port, info = msg:match("%[Entry%] (.-):(%S*)%s(.*)") 350 | if address and port and info then 351 | local e = { 352 | address = address, 353 | port = port, 354 | info = info, 355 | } 356 | table.insert( listOnline, e ) 357 | if self.callbacks.newEntryOnline then 358 | self.callbacks.newEntryOnline( e ) 359 | end 360 | return true 361 | else 362 | print("[ADVERTISE] Reply:", msg ) 363 | end 364 | end 365 | return false 366 | end 367 | 368 | function advertise:getServerList( where ) 369 | where = where or "both" 370 | 371 | if string.lower( where ) == "both" then 372 | local t = {} 373 | for k, v in ipairs( listLAN ) do 374 | t[k] = v 375 | end 376 | for k, v in ipairs( listOnline ) do 377 | t[k + #listLAN] = v 378 | end 379 | return t 380 | elseif string.lower( where ) == "lan" then 381 | return listLAN 382 | elseif string.lower( where ) == "online" then 383 | return listOnline 384 | end 385 | end 386 | 387 | return advertise 388 | -------------------------------------------------------------------------------- /server.lua: -------------------------------------------------------------------------------- 1 | local BASE = (...):match("(.-)[^%.]+$") 2 | local BASE_SLASH = BASE:sub(1,#BASE-1) .. "/" 3 | 4 | local socket = require("socket") 5 | 6 | local User = require( BASE .. "user" ) 7 | local CMD = require( BASE .. "commands" ) 8 | 9 | --local advertiseLAN = require( BASE_SLASH .. "serverlist/advertiseLAN" ) 10 | 11 | local utility = require( BASE .. "utility" ) 12 | 13 | local ADVERTISEMENT_UPDATE_TIME = 60 14 | 15 | local Server = {} 16 | Server.__index = Server 17 | 18 | local userList = {} 19 | local numberOfUsers = 0 20 | local userListByName = {} 21 | local authorizationTimeout = {} 22 | 23 | local MAX_PLAYERS = 16 24 | 25 | local AUTHORIZATION_TIMEOUT = 2 26 | 27 | local PINGTIME = 5 28 | local SYNCH_PINGS = true 29 | 30 | function Server:new( maxNumberOfPlayers, port, pingTime, portUDP ) 31 | local o = {} 32 | setmetatable( o, self ) 33 | 34 | print("[NET] Initialising Server...") 35 | o.conn = assert(socket.bind("*", port)) 36 | o.conn:settimeout(0) 37 | if o.conn then 38 | print("[NET]\t-> started.") 39 | end 40 | 41 | o.callbacks = { 42 | received = nil, 43 | disconnectedUser = nil, 44 | disconnectedUnsynchedUser = nil, 45 | authorize = nil, 46 | customDataChanged = nil, 47 | userFullyConnected = nil, 48 | } 49 | 50 | userList = {} 51 | userListByName = {} 52 | numberOfUsers = 0 53 | PINGTIME = pingTime or 5 54 | 55 | MAX_PLAYERS = maxNumberOfPlayers or 16 56 | 57 | o.port = port 58 | o.portUDP = portUDP 59 | o.advertisement = {} 60 | 61 | return o 62 | end 63 | 64 | function Server:update( dt ) 65 | if self.conn then 66 | 67 | local newConnection = self.conn:accept() 68 | if newConnection then 69 | newConnection:settimeout(0) 70 | 71 | local id = findFreeID() 72 | local newUser = User:new( newConnection, "Unknown", id ) 73 | 74 | userList[id] = newUser 75 | 76 | numberOfUsers = numberOfUsers + 1 77 | 78 | self:newUser( newUser ) 79 | 80 | print( "[NET] Client attempting to connect (" .. id .. ")" ) 81 | end 82 | 83 | for k, u in pairs(userList) do 84 | 85 | local data, msg, partOfLine = u.connection:receive( 9999 ) 86 | if data then 87 | u.incoming.part = u.incoming.part .. data 88 | else 89 | 90 | if msg == "timeout" then -- only part of the message could be received 91 | if #partOfLine > 0 then 92 | -- store for later user: 93 | u.incoming.part = u.incoming.part .. partOfLine 94 | end 95 | elseif msg == "closed" then -- something closed the connection 96 | numberOfUsers = numberOfUsers - 1 97 | 98 | self:disconnectedUser( u ) 99 | 100 | userList[k] = nil 101 | if userListByName[ u.playerName ] then 102 | userListByName[ u.playerName ] = nil 103 | end 104 | else 105 | print("[NET] Err Received:", msg, data) 106 | end 107 | end 108 | 109 | if not u.incoming.length then 110 | if #u.incoming.part >= 1 then 111 | local headerLength = nil 112 | u.incoming.length, headerLength = utility:headerToLength( u.incoming.part:sub(1,5) ) 113 | if u.incoming.length and headerLength then 114 | u.incoming.part = u.incoming.part:sub(headerLength + 1, #u.incoming.part ) 115 | end 116 | end 117 | end 118 | 119 | -- if I already know how long the message should be: 120 | if u.incoming.length then 121 | if #u.incoming.part >= u.incoming.length then 122 | -- Get actual message: 123 | local currentMsg = u.incoming.part:sub(1, u.incoming.length) 124 | 125 | -- Remember rest of already received messages: 126 | u.incoming.part = u.incoming.part:sub( u.incoming.length + 1, #u.incoming.part ) 127 | 128 | command, content = string.match( currentMsg, "(.)(.*)") 129 | command = string.byte( command ) 130 | 131 | self:received( command, content, u ) 132 | u.incoming.length = nil 133 | end 134 | end 135 | 136 | -- Fallback for backwards compability with clients which don't send an authorization 137 | -- request: 138 | if not u.authorized then 139 | u.authorizationTimeout = u.authorizationTimeout - dt 140 | -- Force authorization test now, with empty auth message: 141 | if u.authorizationTimeout < 0 then 142 | self:authorize( u, "" ) 143 | end 144 | end 145 | 146 | -- Every PINGTIME seconds, ping the user and wait for a pong. 147 | -- Check if we already pinged and if not, send a ping: 148 | if not u.ping.waitingForPong then 149 | if u.ping.timer > PINGTIME then 150 | self:send( CMD.PING, "" ) 151 | u.ping.timer = 0 152 | u.ping.waitingForPong = true 153 | end 154 | else -- Otherwise, wait for pong. If it doesn't come, kick user. 155 | if u.ping.timer > 3*PINGTIME then 156 | self:kickUser( u, "Timeout. Didn't respond to ping." ) 157 | end 158 | end 159 | u.ping.timer = u.ping.timer + dt 160 | end 161 | 162 | return true 163 | else 164 | return false 165 | end 166 | end 167 | 168 | function Server:received( command, msg, user ) 169 | if command == CMD.PONG then 170 | if user.ping.waitingForPong then 171 | user.ping.pingReturnTime = math.floor(1000*user.ping.timer+0.5) 172 | user.ping.timer = 0 173 | user.ping.waitingForPong = false 174 | -- let all users know about this user's pingtime: 175 | if SYNCH_PINGS then 176 | self:send( CMD.USER_PINGTIME, user.id .. "|" .. user.ping.pingReturnTime ) 177 | end 178 | end 179 | elseif command == CMD.PLAYERNAME then 180 | 181 | local name, authRequest = msg:match("(.-)|(.*)") 182 | if not name or not authRequest then 183 | name = msg 184 | end 185 | 186 | -- Check if there is another user with this name. 187 | -- If so, increase the number at the end of the name... 188 | while userListByName[ msg ] do 189 | -- Get a possible number at the end of the username: 190 | local base, num = msg:match( "(.+)([%d]+)$" ) 191 | if num then 192 | num = tonumber(num) + 1 193 | else 194 | -- Start with 'name'2: 195 | base = msg 196 | num = 2 197 | end 198 | msg = base .. num 199 | end 200 | 201 | user:setPlayerName( msg ) 202 | if self.callbacks.newPlayername then 203 | self.callbacks.newPlayername( user ) 204 | end 205 | userListByName[ user.playerName ] = user 206 | 207 | -- Let user know about the (possibly corrected) username and his 208 | -- client id: 209 | self:send( CMD.PLAYERNAME, user.id .. "|" .. user.playerName, user ) 210 | 211 | -- Let all users know about the new user... 212 | self:send( CMD.NEW_PLAYER, user.id .. "|" .. user.playerName ) 213 | 214 | self:synchronizeUser( user ) 215 | 216 | elseif command == CMD.AUTHORIZATION_REQUREST then 217 | if not user.authorized then 218 | self:authorize( user, msg ) 219 | end 220 | elseif command == CMD.USER_VALUE then 221 | local keyType, key, valueType, value = string.match( msg, "(.*)|(.*)|(.*)|(.*)" ) 222 | key = stringToType( key, keyType ) 223 | value = stringToType( value, valueType ) 224 | 225 | -- Remember what the value used to be: 226 | local prevValue = user.customData[key] 227 | -- Set new value: 228 | user.customData[key] = value 229 | 230 | -- Let others know about this value: 231 | self:send( CMD.USER_VALUE, user.id .. "|" .. msg ) 232 | 233 | if self.callbacks.customDataChanged then 234 | self.callbacks.customDataChanged( user, value, key, prevValue ) 235 | end 236 | 237 | elseif self.callbacks.received then 238 | -- If the command is not known by the engine, then send it on to the above layer: 239 | self.callbacks.received( command, msg, user ) 240 | end 241 | end 242 | 243 | function Server:synchronizeUser( user ) 244 | 245 | -- Synchronize: Send all other users to this user: 246 | for k, u in pairs( userList ) do 247 | if u.synchronized then 248 | self:send( CMD.NEW_PLAYER, u.id .. "|" .. u.playerName, user ) 249 | 250 | -- Synchronize any custom data of all users: 251 | for key, value in pairs( u.customData ) do 252 | local keyType = type( key ) 253 | local valueType = type( value ) 254 | local msg = u.id .. "|" .. keyType .. "|" .. tostring(key) .. 255 | "|" .. valueType .. "|" .. tostring(value) 256 | self:send( CMD.USER_VALUE, msg, user ) 257 | end 258 | end 259 | end 260 | 261 | -- Send this new user to the user as well (let him know about himself) 262 | self:send( CMD.NEW_PLAYER, user.id .. "|" .. user.playerName, user ) 263 | 264 | if self.callbacks.synchronize then 265 | self.callbacks.synchronize( user ) 266 | end 267 | 268 | user.synchronized = true 269 | 270 | -- Let the program know that this user is now considered fully synchronized 271 | if self.callbacks.userFullyConnected then 272 | self.callbacks.userFullyConnected( user ) 273 | end 274 | 275 | print("[NET] New Client! (" .. numberOfUsers .. ")" ) 276 | end 277 | 278 | 279 | function Server:send( command, msg, user ) 280 | -- Send to only one user: 281 | if user then 282 | local fullMsg = string.char(command) .. (msg or "") --.. "\n" 283 | 284 | local len = #fullMsg 285 | assert( len < 256^4, "Length of packet must not be larger than 4GB" ) 286 | 287 | fullMsg = utility:lengthToHeader( len ) .. fullMsg 288 | 289 | --user.connection:send( string.char(command) .. (msg or "") .. "\n" ) 290 | local result, err, num = user.connection:send( fullMsg ) 291 | while err == "timeout" do 292 | fullMsg = fullMsg:sub( num+1, #fullMsg ) 293 | result, err, num = user.connection:send( fullMsg ) 294 | end 295 | 296 | return 297 | end 298 | 299 | -- If no user is given, broadcast to all. 300 | for k, u in pairs( userList ) do 301 | if u.connection and u.synchronized then 302 | self:send( command, msg, u ) 303 | end 304 | end 305 | end 306 | 307 | function Server:newUser( user ) 308 | -- Wait for AUTHORIZATION_TIMEOUT seconds before forcing authorization process: 309 | user.authorizationTimeout = AUTHORIZATION_TIMEOUT 310 | end 311 | 312 | function Server:authorize( user, authMsg ) 313 | local authorized = true 314 | local reason = "" 315 | 316 | if numberOfUsers > MAX_PLAYERS then 317 | authorized = false 318 | reason = "Server full!" 319 | end 320 | 321 | if authorized then 322 | if self.callbacks.authorize then 323 | authorized, reason = self.callbacks.authorize( user, authMsg ) 324 | end 325 | end 326 | 327 | if authorized then 328 | self:send( CMD.AUTHORIZED, "true|" .. user.id, user ) 329 | user.authorized = true 330 | else 331 | self:send( CMD.AUTHORIZED, "false|" .. reason, user ) 332 | user.connection:shutdown() 333 | end 334 | end 335 | 336 | function Server:disconnectedUser( user ) 337 | 338 | -- If the other clients already know about this client, 339 | -- then tell them to delete him. 340 | if user.synchronized then 341 | print("[NET] Client left (" .. user.id .. ")" ) 342 | self:send( CMD.PLAYER_LEFT, tostring(user.id) ) 343 | 344 | if self.callbacks.disconnectedUser then 345 | self.callbacks.disconnectedUser( user ) 346 | end 347 | else 348 | print("[NET] Client left before synchronizing (" .. user.id .. ")" ) 349 | if self.callbacks.disconnectedUnsynchedUser then 350 | self.callbacks.disconnectedUnsynchedUser( user ) 351 | end 352 | end 353 | end 354 | 355 | -- Find an empty slot in the user list: 356 | function findFreeID() 357 | for k = 1, numberOfUsers + 100 do 358 | if not userList[k] then 359 | return k 360 | end 361 | end 362 | end 363 | 364 | function Server:getUsers() 365 | return userList 366 | end 367 | function Server:getNumUsers() 368 | return numberOfUsers 369 | end 370 | 371 | function Server:kickUser( user, msg ) 372 | self:send( CMD.KICKED, msg, user ) 373 | user.connection:shutdown() 374 | end 375 | 376 | function Server:close() 377 | if self.conn then 378 | for k, u in pairs( userList ) do 379 | u.connection:shutdown() 380 | end 381 | self.conn:close() 382 | end 383 | self.conn = nil 384 | end 385 | 386 | function Server:setUserValue( user, key, value ) 387 | 388 | assert( user.synchronized, "Do not use server:setUserValue() before synchronization is done." ) 389 | 390 | user.customData[key] = value 391 | 392 | -- Broadcast to other users: 393 | local keyType = type( key ) 394 | local valueType = type( value ) 395 | self:send( CMD.USER_VALUE, user.id .. "|" .. keyType .. "|" .. tostring(key) .. 396 | "|" .. valueType .. "|" .. tostring(value) ) 397 | end 398 | 399 | return Server 400 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Affair - Löve Networking Library # 2 | 3 | A networking library for the awesome [Löve engine](http://love2d.org). 4 | 5 | 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](https://love2d.org/wiki/LUBE), [Noobhub](https://love2d.org/wiki/Noobhub), or you could even use plain [Luasocket](http://w3.impa.br/~diego/software/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. 6 | 7 | ## Features: ## 8 | - Callbacks for important events (new user, disconnected etc.) can be defined. 9 | - Automatic synchronizing of player names and player IDs 10 | - Server is independent of Löve and can be run as a dedicated, headless, plain-Lua server (example included). 11 | - Online serverlist 12 | - LAN serverlist (UDP-Broadcast) 13 | - 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. 14 | - Calculates and stores ping time of players. 15 | - Automatic handling of usernames. If a name appears multiple times, the library automatically appends numbers and increments them. 16 | 17 | ## Example: ## 18 | 19 | The lib comes with an example (main.lua). 20 | Run a server using: 21 | ```bash 22 | love . --server 23 | ``` 24 | Connect a user by calling: 25 | ```bash 26 | love . --client ADDRESS 27 | ``` 28 | ADDRESS is the IP address. Defaults to 'localhost'. 29 | 30 | Default port is 3410. 31 | 32 | You can also create a server _and_ connect a client to it at the same time: 33 | ```bash 34 | love . --server --client localhost 35 | ``` 36 | 37 | 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.) 38 | Run: 39 | ```bash 40 | lua examples/dedicated.lua 41 | ``` 42 | Then connect a client to it by running the client example above. 43 | 44 | ## Server: ## 45 | 46 | A server is started using the **network:startServer** function: 47 | 48 | ``` 49 | server, err = network:startServer( numberOfPlayers, port, pingUpdate, portUDP ) 50 | ``` 51 | - **numberOfPlayers** is the maximum number of clients that may connect to the server (default 16). 52 | - **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) 53 | - **pingUpdate** specifies how often the server should ping clients. (To check for timeouts, for example. Value in seconds, default is 5 seconds.) 54 | - **portUDP** sets the port number to be used for creating a LAN server list. Use a value between 1025 and 65535, default is 3410) 55 | 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.) 56 | If the function fails, it returns nil and an err will be filled with the error message. 57 | Most of the time, port, pingUpdate and portUDP can be left at their defaults, so you would call the function like so: 58 | ```lua 59 | -- creates a server which allows a maximum of 8 connections. 60 | server, err = network:startServer( 8 ) 61 | if server then 62 | ... 63 | else 64 | print("Could not start server: " .. err ) 65 | end 66 | ``` 67 | 68 | 69 | ### Callbacks: ### 70 | 71 | 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: 72 | 73 | ```lua 74 | server, err = network:startServer( ... ) 75 | if server then 76 | server.callbacks.received = serverReceive 77 | server.callbacks.authorize = authorize 78 | end 79 | ``` 80 | 81 | **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. 82 | 83 | **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. 84 | 85 | **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. 86 | 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. 87 | 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. 88 | 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. 89 | 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 90 | 91 | **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 92 | 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. 93 | 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. 94 | _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. 95 | 96 | **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. 97 | 98 | **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: 99 | ```lua 100 | function disconnected( user ) 101 | print( user.playername .. " has has left. (ID: " .. user.id .. ")" ) 102 | end 103 | ``` 104 | 105 | ## Client: ## 106 | 107 | A client is started (and connected to an already running server) by calling **network:startClient**. 108 | 109 | ``` 110 | client, err = network:startClient( address, playername, port, authMsg ) 111 | ``` 112 | - **address**: The IP v4 Address to connect to (example: "192.168.0.10", default: "localhost"). 113 | - **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. 114 | - **port**: The port the server is running on. Make sure this is the same as the server's port setting! (default: 3410) 115 | - **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. 116 | 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. 117 | 118 | ### Callbacks: ### 119 | 120 | 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: 121 | 122 | ```lua 123 | client, err = network:startClient( ... ) 124 | if client then 125 | client.callbacks.connected = connect 126 | client.callbacks.received = clientReceive 127 | end 128 | ``` 129 | 130 | **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. 131 | 132 | **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. 133 | 134 | **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. 135 | 136 | **client.callbacks.disconnected():** Called when the client has been disconnected for some reason. 137 | 138 | **client.callbacks.newUser( user ):** Called on all clients when a new user has been synchronized. 139 | Note: This is not called on the client who is joining (i.e. the one who has just been synchronized). 140 | 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. 141 | 142 | ## Remarks: ## 143 | 144 | 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. 145 | 146 | ## Server lists: ## 147 | ### Online: ### 148 | 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](https://github.com/Germanunkol/AffairMainServer) into some folder on that main server (The only requirement is that the folder is visible to anyone). 149 | On your game's server, call the following function (after having created a server using network:startServer): 150 | 151 | **server:advertise( data, id, url, portUDP )** 152 | - 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. 153 | - id: A name which identifies the game 154 | - 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_. 155 | - 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. 156 | 157 | **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. 158 | 159 | 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. 160 | 161 | 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. 162 | 163 | If you want to stop advertising the server, call (for example because a round has started and you want no more players to join): 164 | **server:unAdvertise()** 165 | 166 | **Example:** 167 | ```lua 168 | function startServer() 169 | -- Attempt to create a server: 170 | server, err = network:startServer( NUMBER_OF_PLAYERS, PORT, PING_UPDATE_TIME ) 171 | 172 | if server then 173 | -- These callbacks are called when a new user connects or a user disconnected: 174 | server.callbacks.userFullyConnected = connected 175 | server.callbacks.disconnectedUser = disconnected 176 | -- Start advertising this server, so others can join: 177 | server:advertise( "Players:0", "ExampleServer", MAIN_SERVER_ADDRESS ) 178 | else 179 | print("Error starting server:", err) 180 | love.event.quit() 181 | end 182 | end 183 | 184 | -- Update number of players on the serverlist: 185 | function connected() 186 | local players = network:getUsers() 187 | -- Only update the data field in the advertisement, leave the id and URL the same: 188 | server:advertise( "Players:" .. #players ) 189 | end 190 | function disconnected() 191 | local players = network:getUsers() 192 | -- Only update the data field in the advertisement, leave the id and URL the same: 193 | server:advertise( "Players:" .. #players - 1 ) 194 | end 195 | ``` 196 | 197 | ### LAN: ### 198 | 199 | When calling the **server:advertise** function above, the server will also advertise itself in your Local Area Network. 200 | This uses a UDP port to accept messages from clients. The port can be set as the fourth argument in **network:startServer** (see above). 201 | 202 | On the client, you can start looking for LAN servers using: 203 | ```lua 204 | network:requestServerListLAN( id, portUDP ) 205 | ``` 206 | - **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 207 | - **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. 208 | 209 | 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. 210 | --------------------------------------------------------------------------------