-- Hub-and-spoke networking hub -- Manages client connections and routes messages between them net = require "net" log = require "log" world = require "world" -- Register message types that clients sends to the hub -- Client -> Hub: RequestEntities has no payload (empty table is fine) net.register_message("RequestEntities", {}) -- Envelope used for all logical messages routed through the hub net.register_message("routed_message", { required: { message_type: "string" from: "string" } optional: { data: "table" } }) net.register_message("Join", { required: { name: "string" } }) -- Hub -> Client: RespondEntities sends the current entities list (array) net.register_message("RespondEntities", { optional: { entities: "table" } }) -- Hub -> Client: CreateEntity announces a new entity for a given peerid net.register_message("CreateEntity", { required: { peerid: "string" } }) class Hub new: => @peer = nil @clients = {} -- client_id -> connection @client_names = {} -- client_id -> name (optional) @initialized = false @routes = {} @on_connect_callbacks = {} @on_disconnect_callbacks = {} initialize: => if @initialized return --@attempts = 0 --create_peer = () -> --@peer = net.Peer! --fail_peer_creation = () -> --@attempts += 1 --while @attempts < 4 --xpcall(create_peer, fail_peer_creation) --log.info("Creating peer attempt " .. @attempts .. " / 4", {"net","server"}) --if @peer == nil --error("Failed to create peer 4 times...") @peer = net.Peer! log.info("Hub peer created: #{@peer.id}", {"server", "net"}) -- Listen for incoming connections @peer\on "connection", (peer, message) -> client_id = message.data.dest -- the other side of the connection? log.info("Client connected: " .. tostring(peer) .. ":" .. tostring(message), {"server", "net"}) conn = message.data @clients[client_id] = conn -- Set up connection handlers -- When the connection receives data, the callback is invoked with -- (connection, message_array), where message_array = { msgname, msgdata }. -- We only care about the logical client id and the logical message array, -- so forward just those to handle_message. This keeps the JS bridge -- signature Hub.handle_message(from_client, msg) where msg is the -- array { msg_type, msg_data }. conn\on "data", (conn, message) -> log.info("On data got arguments:" .. tostring(conn) .. "," .. tostring(message), {"server","net"}) @handle_message(client_id, message) conn\on "close", () -> @handle_disconnect(client_id) -- Notify connection callbacks for callback in *@on_connect_callbacks callback(client_id) @initialized = true require("server.init") handle_message: (from_client, message) => -- message is the array {msg_type, msg_data} that was sent over the wire log.info("Hub received message: #{message} from #{from_client}", {"net", "server"}) msg_type = message[1] msg_data = message[2] -- Build routed_message-style envelope for validation routed = { message_type: msg_type from: from_client data: msg_data } -- Validate envelope ok, err = pcall(net.validate, "routed_message", routed) if not ok log.error("Invalid routed_message from " .. tostring(from_client) .. ": " .. tostring(routed), {"net", "server"}) return -- Validate payload for the specific message type ok, err = pcall(net.validate, msg_type, msg_data) if not ok log.error("Invalid " .. tostring(msg_type) .. " payload from " .. tostring(from_client) .. ": " .. tostring(err), {"net", "server"}) return if msg_type == "Join" log.info("Hub handling Join from " .. tostring(from_client) .. " with data=" .. tostring(msg_data), {"net", "server", "debug"}) world.domain = "server" if @routes[msg_type] -- routes[msg_type] is a flat map id -> callback(from_client, data) for _, callback in pairs(@routes[msg_type]) callback(from_client, msg_data) else log.warn("No routes for " .. tostring(msg_type), {"net","server"}) world.domain = "client" listen: (message_type, id, callback) => @routes[message_type] = @routes[message_type] or {} id = id or "default" @routes[message_type][id] = callback log.info("Hub listening for #{message_type}", {"net", "server"}) id defen: (message_type, id) => @routes[message_type][id] = nil send: (client_id, message_type, data) => conn = @clients[client_id] if not conn log.warn("Cannot send to unknown client: #{client_id}", {"server", "net"}) return log.info("Hub sending #{message_type} to #{client_id}", {"net", "server"}) conn\send({message_type, data}) broadcast: (message_type, data) => nclients = 0 for _, _ in pairs(@clients) nclients += 1 log.info("Hub broadcasting #{message_type} to " .. tostring(nclients) .. " clients", {"net", "server"}) for clientid, conn in pairs(@clients) conn\send({message_type, data}) handle_disconnect: (client_id) => log.info("Client disconnected: #{client_id}", {"server", "net"}) @clients[client_id] = nil @client_names[client_id] = nil -- Notify disconnect callbacks for callback in *@on_disconnect_callbacks callback(client_id) on_connect: (callback) => table.insert(@on_connect_callbacks, callback) on_disconnect: (callback) => table.insert(@on_disconnect_callbacks, callback) get_clients: => clients = {} for id, conn in pairs @clients table.insert(clients, { id: id name: @client_names[id] }) clients pump: => net.pump! {:Hub}