local enet = require("enet") local socket = require("socket") local util = require("util") local common = require("common") local save_file = require("save_file") local server = {} local function migrate_save(save) save.players = save.players or {} end local function save_data(srv) -- TODO: handle failure save_file.write(srv.save_file, srv.data) end local function get_player(srv, name) for _, player in ipairs(srv.data.players) do if player.name == name then return player end end end local function get_players(srv) local players = {} for _, player in ipairs(srv.data.players) do table.insert(players, { name = player.name, active = srv.players[player.name] ~= nil, }) end return players end local function get_info_pkt(srv) return util.json_enc({ type = "client_info", players = get_players(srv), }) end local function broadcast_info(srv) local pkt = get_info_pkt(srv) for _, clt in pairs(srv.clients) do clt.peer:send(pkt) end end local function create_player(srv, name) local player = { name = name } table.insert(srv.data.players, player) print("[server] created player " .. name) save_data(srv) return player end local function disconnect(srv, clt) srv.clients[clt.peer] = nil if clt.player then srv.players[clt.player.name] = nil end broadcast_info(srv) end local function select_player(srv, clt, pkt) if #pkt.name > 128 then return "name_too_long" end if srv.players[pkt.name] then return nil, "already_active" end local player = get_player(srv, pkt.name) if pkt.create and player then return nil, "already_exists" end if not pkt.create and not player then return nil, "not_exists" end if pkt.create then player = create_player(srv, pkt.name) end return player end function server.create(filename, match_addr) local srv = {} srv.host = enet.host_create() srv.secret = util.rand_string(common.secret_len) srv.clients = {} srv.players = {} srv.match = srv.host:connect(match_addr or common.default_match_addr) srv.match_req = socket.gettime() local save, err = save_file.read(filename) if err then return nil, err end srv.save_file = filename srv.data = save migrate_save(save) return srv end local function handle_match(srv, pkt) if pkt.type == "server_match" then local game_id = type(pkt.game_id) == "string" and util.base64_dec(pkt.game_id) if not game_id then print("[server] server_match: invalid game_id") return end if srv.game_id then print("[server] server_match: received while game already running") return end srv.game_id = game_id srv.invite = util.base64_enc(srv.game_id .. srv.secret) elseif pkt.type == "server_join" then if type(pkt.peer_addr) ~= "string" then print("[server] server_join: invalid peer_addr") return end srv.host:connect(pkt.peer_addr) end end local function handle_client(srv, peer, pkt) if pkt.type == "server_hi" then local secret = type(pkt.secret) == "string" and util.base64_dec(pkt.secret) if not secret then print("[server] server_hi: invalid secret") return end if srv.clients[peer] then print("[server] server_hi: client already connected") return end if secret == srv.secret then print("[server] auth success " .. tostring(peer)) local clt = { peer = peer } srv.clients[peer] = clt util.send(peer, { type = "client_hi", }) peer:send(get_info_pkt(srv)) else print("[server] auth failure " .. tostring(peer)) util.send(peer, { type = "client_reject" }) peer:disconnect_later() end end local clt = srv.clients[peer] if not clt then print("[server] dropping unauthenicated packet from " .. tostring(peer)) return end if pkt.type == "server_player" then if clt.player then print("[server] dropping server_player from already authenticated player") return end if type(pkt.name) ~= "string" or type(pkt.create) ~= "boolean" then print("[server] server_player: invalid packet") return end local player, err = select_player(srv, clt, pkt) if err then print("[server] failed to select player " .. tostring(clt.peer)) util.send(clt.peer, { type = "client_player_fail", error = err, }) else print("[server] select player " .. tostring(clt.peer) .. ": " .. player.name) srv.players[player.name] = clt clt.player = player util.send(clt.peer, { type = "client_player", name = player.name, }) broadcast_info(srv) end end end function server.update(srv) local event = srv.host:service(20) while event do if event.type == "receive" then local pkt = util.json_dec(event.data) if pkt then if event.peer == srv.match then handle_match(srv, pkt) else handle_client(srv, event.peer, pkt) end end elseif event.type == "connect" then if event.peer == srv.match then util.send(srv.match, { type = "match_register" }) end print("[server] connect " .. tostring(event.peer)) elseif event.type == "disconnect" then print("[server] disconnect " .. tostring(event.peer)) if event.peer == srv.match then -- TODO else local clt = srv.clients[event.peer] if clt then disconnect(srv, clt) end end end event = srv.host:service() end end function server.match_status(srv) if srv.game_id then return "active", srv.invite elseif srv.match_req + 3 >= socket.gettime() then return "wait" else return "fail" end end function server.close(srv) save_data(srv) local peers = srv.host:peer_count() for i = 1, peers do srv.host:get_peer(i):disconnect() end srv.host:flush() srv.host:destroy() end return server