diff options
| author | Lizzy Fleckenstein <lizzy@vlhl.dev> | 2026-06-03 22:31:42 +0200 |
|---|---|---|
| committer | Lizzy Fleckenstein <lizzy@vlhl.dev> | 2026-06-03 23:30:17 +0200 |
| commit | 341af788ffb60b1066f7735c10a2ef8480ec0aa9 (patch) | |
| tree | b1acbfe48184a263098e6146922730dd7ece8808 | |
| parent | a42c94e103ecf7cb365a8888c3f5afc785def284 (diff) | |
| download | r6p-main.tar.xz | |
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | client.lua | 32 | ||||
| -rw-r--r-- | main.lua | 238 | ||||
| -rwxr-xr-x | matchsrv.lua | 8 | ||||
| -rw-r--r-- | save_file.lua | 37 | ||||
| -rw-r--r-- | server.lua | 150 | ||||
| -rwxr-xr-x | standalone_server.lua | 47 | ||||
| -rw-r--r-- | ui.lua | 21 | ||||
| -rw-r--r-- | util.lua | 12 |
9 files changed, 445 insertions, 101 deletions
@@ -1 +1,2 @@ build/ +saves/ @@ -29,7 +29,7 @@ function client.join(invite, match_addr) local clt = create_client(secret) clt.match = clt.host:connect(match_addr or common.default_match_addr) - clt.match:send(util.json_enc({ type = "match_join", game_id = game_id })) + util.send(clt.match, { type = "match_join", game_id = game_id }) clt.match_req = socket.gettime() clt.game_id = game_id clt.status = "wait_match" @@ -59,6 +59,12 @@ local function handle_server(clt, pkt) clt.status = "active" elseif pkt.type == "client_reject" then clt.status = "fail_server" + elseif pkt.type == "client_info" then + clt.info = pkt + elseif pkt.type == "client_player" then + clt.player = pkt.name + elseif pkt.type == "client_player_fail" then + clt.player_error = pkt.error end end @@ -78,9 +84,9 @@ function client.update(clt) end elseif event.type == "connect" then if event.peer == clt.match and clt.status == "wait_match" then - clt.match:send(util.json_enc({ type = "match_join", game_id = util.base64_enc(clt.game_id) })) + util.send(clt.match, { type = "match_join", game_id = util.base64_enc(clt.game_id) }) elseif event.peer == clt.server and clt.status == "wait_server" then - clt.server:send(util.json_enc({ type = "server_hi", secret = util.base64_enc(clt.secret) })) + util.send(clt.server, { type = "server_hi", secret = util.base64_enc(clt.secret) }) else event.peer:disconnect_now() end @@ -103,24 +109,20 @@ function client.status(clt) end return clt.status +end - - -- wait_match - -- wait_server - - -- timeout_match - -- fail_match - -- timeout_server - -- fail_server - - -- active - -- disco +function client.select_player(clt, name, create) + util.send(clt.server, { + type = "server_player", + name = name, + create = create, + }) end function client.close(clt) if clt.match then clt.match:disconnect() end if clt.server then clt.server:disconnect() end - clt.host:service() + clt.host:flush() clt.host:destroy() end @@ -21,42 +21,40 @@ local main_menu = require("main_menu") local ui = require("ui") local color = ui.color -local mm local clt -local active_ui -local loading_text, loading_ui -local error_text, error_ui -local connected = false -local last_status +local dialog = {} +local last_status, last_info -local function show_error(msg) - error_text.text:set(msg) - active_ui = error_ui +local function show(dg, ...) + dialog.active = dg + if dg.show then + dg:show(...) + end end -local function join_game(invite) +local function connect(invite) local c, err = client.join(invite) if err then - show_error("Invalid invite") + show(dialog.error, "Invalid invite") else clt = c - active_ui = loading_ui end end local function disconnect(msg) client.close(clt) clt = nil - connected = false + last_status = nil if msg then - show_error(msg) + show(dialog.error, msg) else - active_ui = mm + show(dialog.main_menu, msg) end end -local function loading_status(status) - loading_text.text:set(status) +local function select_player(name, create) + client.select_player(clt, name, create) + show(dialog.loading, "Joining Lobby...") end local function update_client() @@ -75,68 +73,195 @@ local function update_client() disconnect("Lost connection to server (Server shut down?)") elseif status ~= last_status then if status == "active" then - loading_status("Connected to server!") - connected = true + show(dialog.loading, "Waiting for info...") elseif status == "wait_match" then - loading_status("Waiting for match server...") + show(dialog.loading, "Waiting for match server...") elseif status == "wait_server" then - loading_status("Waiting for server...") + show(dialog.loading, "Waiting for server...") end end + if not clt then return end + + if clt.player_error then + show(dialog.error, clt.player_error, dialog.select_player) + clt.player_error = nil + elseif clt.player and (not in_lobby or (in_lobby and last_info ~= clt.info)) then + show(dialog.lobby) + elseif not clt.player and last_info ~= clt.info and (not last_info or dialog.active == dialog.select_player) then + show(dialog.select_player) + end + + last_info = clt.info last_status = status end -function love.load() - love.graphics.setBackgroundColor(color(0xffffff)) - love.window.setTitle("RAINBOW SIX: PANOPTICON") +local function create_dialog_box(title, elem, width) + return ui.new(ui.center_y(ui.stack_x( + ui.flex(1), + ui.flex(width or 2, main_menu.box(title, elem)), + ui.flex(1) + ))) +end - mm = main_menu.create({ - join_game = join_game, - }) +local function create_info_box(title, action_name, action, show) + local box = main_menu.box(title, ui.button(action_name, action)) + local dg = ui.new(ui.center_x(ui.center_y(box))) + function dg.set_title(self, x) + box.box_title.text:set(x) + end + dg.show = show + return dg +end - local loading_box = main_menu.box( - "Loading...", - ui.button("Cancel", function() - disconnect() - end) +local function create_loading() + return create_info_box("Loading...", "Cancel", + function() disconnect() end, + function(self, msg) + self:set_title(msg) + end ) - loading_text = loading_box.box_title - loading_ui = ui.new(ui.center_x(ui.center_y(loading_box))) +end - local error_box = main_menu.box( - "Error", - ui.button("Back", function() - active_ui = mm - end) +local function create_error() + local back_menu + return create_info_box("Error", "Back", + function() + show(back_menu or dialog.main_menu) + end, + function(self, msg, back) + self:set_title(msg) + back_menu = back + end ) - error_text = error_box.box_title - error_ui = ui.new(ui.center_x(ui.center_y(error_box))) +end + +local function create_player_indicator(player, text_size) + local name = player.name + if clt and clt.player == player.name then + name = name .. " [YOU]" + end + + local text = ui.text(name, text_size, color(0x000000)) + local status_size = text.text:getHeight()-10 - active_ui = mm + return ui.stack_x( + ui.center_y({ + fill = player.active and color(0x23cf3a) or color(0xc21b3a), + size = { x = status_size, y = status_size } + }), + ui.pad_x(10), + text + ) +end + +local function create_select_player() + local contents = {} + local dg = create_dialog_box("Select Player", contents) + dg.show = function() + local stack = ui.stack_y() + for _, p in ipairs(clt.info.players) do + local button = ui.button_elem(create_player_indicator(p, 25), function() + if not p.active then + select_player(p.name, false) + end + end) + button.line = nil + + if p.active then + button.fill_normal = color(0x122194) + button.fill_hover = color(0x122194) + else + button.fill_normal = color(0x223ebd) + button.fill_hover = color(0x2d95d6) + end + + table.insert(stack, button) + end + local create = ui.button("+", function() + show(dialog.create_player) + end) + create.line = nil + create.fill_normal = color(0x7e1dbf) + create.fill_hover = color(0x9540cf) + table.insert(stack, create) + + contents[1] = stack + end + return dg +end + +local function create_create_player() + local input = ui.input("Player name") + return create_dialog_box("Create Player", ui.stack_y( + input, + ui.pad_y(10), + ui.pad_x(10, + ui.button("Back", function() + show(dialog.select_player) + end), + ui.flex(1, ui.button("Create", function() + if input.input_value ~= "" then + select_player(input.input_value, true) + end + end)) + ) + )) +end + +local function create_lobby() + local player_list = {} + local dg = create_dialog_box("Lobby", ui.stack_y( + player_list, + ui.center_x(ui.button("Disconnect", function() + disconnect() + end)) + ), 3) + function dg.show(self) + local players = ui.stack_y() + for _, p in ipairs(clt.info.players) do + table.insert(players, create_player_indicator(p, 20)) + end + player_list[1] = players + end + return dg +end + +function love.load() + love.graphics.setBackgroundColor(color(0xffffff)) + love.window.setTitle("RAINBOW SIX: PANOPTICON") + + dialog.main_menu = main_menu.create({ join_game = connect }) + dialog.loading = create_loading() + dialog.error = create_error() + dialog.create_player = create_create_player() + dialog.select_player = create_select_player() + dialog.lobby = create_lobby() + + show(dialog.main_menu) end function love.mousepressed(x, y, button) - if active_ui then - ui.mousepressed(active_ui, x, y, button) + if dialog.active then + ui.mousepressed(dialog.active, x, y, button) end end function love.mousereleased(x, y, button) - if active_ui then - ui.mousereleased(active_ui, x, y, button) + if dialog.active then + ui.mousereleased(dialog.active, x, y, button) end end function love.textinput(text) - if active_ui then - ui.textinput(active_ui, text) + if dialog.active then + ui.textinput(dialog.active, text) end end function love.keypressed(key) - if active_ui then - ui.keypressed(active_ui, key) + if dialog.active then + ui.keypressed(dialog.active, key) end end @@ -147,8 +272,15 @@ function love.update() end function love.draw() - if active_ui then - ui.update(active_ui) - ui.render(active_ui) + if dialog.active then + ui.update(dialog.active) + ui.render(dialog.active) + end +end + +function love.quit() + print("shutting down") + if clt then + client.close(clt) end end diff --git a/matchsrv.lua b/matchsrv.lua index 0ffa2b4..315573c 100755 --- a/matchsrv.lua +++ b/matchsrv.lua @@ -22,18 +22,18 @@ local function handle(peer, pkt) local game_id = util.rand_string(common.gameid_len) peer_to_game[peer] = game_id game_to_peer[game_id] = peer - peer:send(util.json_enc({ type = "server_match", game_id = util.base64_enc(game_id) })) + util.send(peer, { type = "server_match", game_id = util.base64_enc(game_id) }) print(peer, "registered game") elseif pkt.type == "match_join" then local game_id = type(pkt.game_id) == "string" and util.base64_dec(pkt.game_id) if game_id then local server = game_id and game_to_peer[game_id] if server then - server:send(util.json_enc({ type = "server_join", peer_addr = tostring(peer) })) - peer:send(util.json_enc({ type = "client_join", peer_addr = tostring(server) })) + util.send(server, { type = "server_join", peer_addr = tostring(peer) }) + util.send(peer, { type = "client_join", peer_addr = tostring(server) }) print(peer, "joined game", server) else - peer:send(util.json_enc({ type = "client_join_fail" })) + util.send(peer, { type = "client_join_fail" }) print(peer, "failed to join game") end end diff --git a/save_file.lua b/save_file.lua new file mode 100644 index 0000000..8dd85f2 --- /dev/null +++ b/save_file.lua @@ -0,0 +1,37 @@ +local util = require("util") + +local function save_file_write(filename, data) + os.rename(filename, filename..".bak") + local f = io.open(filename, "w") + if not f then + return false + end + f:write(util.json_enc(data)) + f:close() + print("[save_file] saved to " .. filename) + return true +end + +local function save_file_read(filename) + local data + local f = io.open(filename, "r") + if f then + data = util.json_dec(f:read("*all")) + f:close() + if not data then + return nil, "save_corrupted" + end + print("[save_file] loaded " .. filename) + else + data = {} + end + if not save_file_write(filename, data) then + return nil, "save_failed_write" + end + return data +end + +return { + write = save_file_write, + read = save_file_read, +} @@ -2,19 +2,108 @@ local enet = require("enet") local socket = require("socket") local util = require("util") local common = require("common") +local save_file = require("save_file") local server = {} -function server.create(match_addr) +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 @@ -57,14 +146,54 @@ local function handle_client(srv, peer, pkt) if secret == srv.secret then print("[server] auth success " .. tostring(peer)) - srv.clients[peer] = { peer = peer } - peer:send(util.json_enc({ type = "client_hi" })) + 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)) - peer:send(util.json_enc({ type = "client_reject" })) + 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) @@ -81,7 +210,7 @@ function server.update(srv) end elseif event.type == "connect" then if event.peer == srv.match then - srv.match:send(util.json_enc({ type = "match_register" })) + util.send(srv.match, { type = "match_register" }) end print("[server] connect " .. tostring(event.peer)) elseif event.type == "disconnect" then @@ -89,7 +218,10 @@ function server.update(srv) if event.peer == srv.match then -- TODO else - srv.clients[event.peer] = nil + local clt = srv.clients[event.peer] + if clt then + disconnect(srv, clt) + end end end event = srv.host:service() @@ -107,6 +239,12 @@ function server.match_status(srv) 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 diff --git a/standalone_server.lua b/standalone_server.lua index a1dd571..9221984 100755 --- a/standalone_server.lua +++ b/standalone_server.lua @@ -1,20 +1,45 @@ #!/usr/bin/env lua5.1 local server = require("server") -local srv = server.create() -local started = false +local srv, err = server.create(assert(arg[1])) +if err then + if err == "save_corrupted" then + print("[standalone_server] savefile corrupted (try restoring backup?)") + elseif err == "save_write_failed" then + print("[standalone_server] failed to open savefile for writing") + else + print(err) + end + os.exit(1) +end -while true do - server.update(srv) - local status, invite = server.match_status(srv) +local function server_loop() + local started = false + while true do + server.update(srv) + local status, invite = server.match_status(srv) - if status == "fail" then - print("failed to register match (match server down?)") - break - elseif status == "active" and not started then - started = true - print("invite: " .. invite) + if status == "fail" then + print("[standalone_server] failed to register match (match server down?)") + return false + elseif status == "active" and not started then + started = true + print("[standalone_server] invite: " .. invite) + end end end +local _, success = xpcall(server_loop, function(err) + if err:find("interrupted!") then + return true + end + print(debug.traceback(err, 2)) + return false +end, srv) + +print("[standalone_server] shutting down") server.close(srv) + +if not success then + os.exit(1) +end @@ -320,19 +320,27 @@ local function ui_text(str, size, col) } end -local function ui_button(label, click) +local function ui_button_elem(elem, click) return { + fill_normal = color(0x808080), + fill_hover = color(0x8080a0), + line_hover = color(0x505050), + line_normal = color(0x505050), fill = function(self, f) - return f.hover and color(0x8080a0) or color(0x808080) + return f.hover and self.fill_hover or self.fill_normal + end, + line = function(self, f) + return f.hover and self.line_hover or self.line_normal end, - line = color(0x505050), events = { hover = true, press = true, click = click, }, - ui_pad_y(5, center_x(ui_pad_x(10, - ui_text(label, 25, color(0x000000)) - ))) + ui_pad_y(5, ui_pad_x(10, elem)) } end +local function ui_button(label, click) + return ui_button_elem(center_x(ui_text(label, 25, color(0x000000))), click) +end + local function ui_input(placeholder) local function is_placeholder(self, focus) return self.input_value == "" and not focus @@ -413,5 +421,6 @@ return { flex = ui_flex, text = ui_text, button = ui_button, + button_elem = ui_button_elem, input = ui_input, } @@ -16,7 +16,11 @@ local function json_enc(x) return json:encode(x) end -local mkdir, rand_string +local function send(peer, x) + peer:send(json_enc(x)) +end + +local rand_string if love then rand_string = function(n) @@ -33,11 +37,6 @@ else local rand_file = rand_file or io.open("/dev/random") return rand_file:read(n) end - -- awful - mkdir = function(x) - local status = os.execute("mkdir -p " .. x) - return status == true - end end return { @@ -47,4 +46,5 @@ return { base64_enc = base64.encode, json_dec = json_dec, json_enc = json_enc, + send = send, } |
