summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLizzy Fleckenstein <lizzy@vlhl.dev>2026-06-03 22:31:42 +0200
committerLizzy Fleckenstein <lizzy@vlhl.dev>2026-06-03 23:30:17 +0200
commit341af788ffb60b1066f7735c10a2ef8480ec0aa9 (patch)
treeb1acbfe48184a263098e6146922730dd7ece8808
parenta42c94e103ecf7cb365a8888c3f5afc785def284 (diff)
downloadr6p-main.tar.xz
add player selectionHEADmain
-rw-r--r--.gitignore1
-rw-r--r--client.lua32
-rw-r--r--main.lua238
-rwxr-xr-xmatchsrv.lua8
-rw-r--r--save_file.lua37
-rw-r--r--server.lua150
-rwxr-xr-xstandalone_server.lua47
-rw-r--r--ui.lua21
-rw-r--r--util.lua12
9 files changed, 445 insertions, 101 deletions
diff --git a/.gitignore b/.gitignore
index 567609b..f52d6c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
build/
+saves/
diff --git a/client.lua b/client.lua
index 3c6cd63..1bf4952 100644
--- a/client.lua
+++ b/client.lua
@@ -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
diff --git a/main.lua b/main.lua
index 238b20b..be0b119 100644
--- a/main.lua
+++ b/main.lua
@@ -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,
+}
diff --git a/server.lua b/server.lua
index 5d1ddd4..9cda5a4 100644
--- a/server.lua
+++ b/server.lua
@@ -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
diff --git a/ui.lua b/ui.lua
index 8ad64e6..60f0833 100644
--- a/ui.lua
+++ b/ui.lua
@@ -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,
}
diff --git a/util.lua b/util.lua
index 0628691..dda250e 100644
--- a/util.lua
+++ b/util.lua
@@ -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,
}