diff options
| author | Lizzy Fleckenstein <lizzy@vlhl.dev> | 2026-06-03 01:15:29 +0200 |
|---|---|---|
| committer | Lizzy Fleckenstein <lizzy@vlhl.dev> | 2026-06-03 01:15:29 +0200 |
| commit | f08683a3775989e749237cd001a8eaf3193d1684 (patch) | |
| tree | fbda564309b9f0f7c66c0bd68a2e8c5f08ff27f3 | |
| parent | cc5b2f31a7abe46147284de869368a0a2c4bcff4 (diff) | |
| download | r6p-f08683a3775989e749237cd001a8eaf3193d1684.tar.xz | |
add main menu
| -rw-r--r-- | client.lua | 13 | ||||
| -rw-r--r-- | conf.lua | 3 | ||||
| -rw-r--r-- | distribute.lua | 48 | ||||
| -rw-r--r-- | main.lua | 154 | ||||
| -rw-r--r-- | main_menu.lua | 48 | ||||
| -rw-r--r-- | ui.lua | 412 |
6 files changed, 675 insertions, 3 deletions
@@ -8,7 +8,7 @@ local client = {} local function create_client(secret) local clt = {} - clt.host = enet.host_create() -- enet.host_create("10.75.98.51:58901") + clt.host = enet.host_create() clt.secret = secret return clt end @@ -20,7 +20,11 @@ local function connect(clt, addr) end function client.join(invite, match_addr) - local invite_dec = base64.decode(invite) + local decode_succ, invite_dec = pcall(base64.decode, invite) + if not decode_succ then + return nil, "invalid_invite" + end + local game_id = invite_dec:sub(1, common.gameid_len) local secret = invite_dec:sub(common.gameid_len+1) @@ -93,7 +97,7 @@ end function client.status(clt) if clt.status == "wait_match" and clt.match_req+3 < socket.gettime() then clt.status = "timeout_match" - elseif clt.status == "wait_server" and clt.match_req+5 < socket.gettime() then + elseif clt.status == "wait_server" and clt.server_req+5 < socket.gettime() then clt.status = "timeout_server" end @@ -113,6 +117,9 @@ function client.status(clt) 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:destroy() end diff --git a/conf.lua b/conf.lua new file mode 100644 index 0000000..d094913 --- /dev/null +++ b/conf.lua @@ -0,0 +1,3 @@ +function love.conf(t) + t.window.resizable = true +end diff --git a/distribute.lua b/distribute.lua new file mode 100644 index 0000000..8c0b76c --- /dev/null +++ b/distribute.lua @@ -0,0 +1,48 @@ +-- min, weight, receive + +local function eliminate(budget, total_weight, elems) + if budget < 0 then + return + end + + if total_weight == 0 then + return 0 + end + + local unit = budget / total_weight + for i, e in ipairs(elems) do + local allocated = unit * e.weight + if e.min > allocated then + e.receive = e.min + table.remove(elems, i) + budget = budget - e.min + total_weight = total_weight - e.weight + return eliminate(budget, total_weight, elems) + end + end + return unit +end + +local function distribute(budget, elems) + local total_weight = 0 + local remaining = {} + for _, v in pairs(elems) do + total_weight = total_weight + v.weight + table.insert(remaining, v) + end + + local unit = eliminate(budget, total_weight, remaining) + if not unit then + for _, e in ipairs(remaining) do + e.receive = e.min + end + return false + end + + for _, e in ipairs(remaining) do + e.receive = unit * e.weight + end + return true +end + +return distribute diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..238b20b --- /dev/null +++ b/main.lua @@ -0,0 +1,154 @@ +function dump(x, idt) + local p = {} + local idt = idt or 0 + for k, v in pairs(x) do + local s + if type(v) == "table" then + s = dump(v, idt+1) + elseif type(v) == "string" then + s = ("\"%s\""):format(v) + else + s = tostring(v) + end + table.insert(p, ("%s%s = %s"):format((" "):rep(4*(idt+1)), k, s)) + end + return ("{\n%s\n%s}"):format(table.concat(p, ", \n"), (" "):rep(4*idt)) +end + +local client = require("client") +local server = require("server") +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 function show_error(msg) + error_text.text:set(msg) + active_ui = error_ui +end + +local function join_game(invite) + local c, err = client.join(invite) + if err then + show_error("Invalid invite") + else + clt = c + active_ui = loading_ui + end +end + +local function disconnect(msg) + client.close(clt) + clt = nil + connected = false + if msg then + show_error(msg) + else + active_ui = mm + end +end + +local function loading_status(status) + loading_text.text:set(status) +end + +local function update_client() + client.update(clt) + + local status = client.status(clt) + if status == "timeout_match" then + disconnect("Failed to connect to match server (match server down?)") + elseif status == "fail_match" then + disconnect("Game not found (invalid invite?)") + elseif status == "timeout_server" then + disconnect("Failed to connect to server (NAT punch failure?)") + elseif status == "fail_server" then + disconnect("Incorrect secret (invalid invite?)") + elseif status == "disco" then + disconnect("Lost connection to server (Server shut down?)") + elseif status ~= last_status then + if status == "active" then + loading_status("Connected to server!") + connected = true + elseif status == "wait_match" then + loading_status("Waiting for match server...") + elseif status == "wait_server" then + loading_status("Waiting for server...") + end + end + + last_status = status +end + +function love.load() + love.graphics.setBackgroundColor(color(0xffffff)) + love.window.setTitle("RAINBOW SIX: PANOPTICON") + + mm = main_menu.create({ + join_game = join_game, + }) + + local loading_box = main_menu.box( + "Loading...", + ui.button("Cancel", function() + disconnect() + end) + ) + loading_text = loading_box.box_title + loading_ui = ui.new(ui.center_x(ui.center_y(loading_box))) + + local error_box = main_menu.box( + "Error", + ui.button("Back", function() + active_ui = mm + end) + ) + error_text = error_box.box_title + error_ui = ui.new(ui.center_x(ui.center_y(error_box))) + + active_ui = mm +end + +function love.mousepressed(x, y, button) + if active_ui then + ui.mousepressed(active_ui, x, y, button) + end +end + +function love.mousereleased(x, y, button) + if active_ui then + ui.mousereleased(active_ui, x, y, button) + end +end + +function love.textinput(text) + if active_ui then + ui.textinput(active_ui, text) + end +end + +function love.keypressed(key) + if active_ui then + ui.keypressed(active_ui, key) + end +end + +function love.update() + if clt then + update_client() + end +end + +function love.draw() + if active_ui then + ui.update(active_ui) + ui.render(active_ui) + end +end diff --git a/main_menu.lua b/main_menu.lua new file mode 100644 index 0000000..73ba7b1 --- /dev/null +++ b/main_menu.lua @@ -0,0 +1,48 @@ +local ui = require("ui") +local color = ui.color + +local function menubox(title, elem) + local box_title = ui.text(title, 30, color(0x000000)) + return { box_title = box_title, fill = color(0xa0a0a0), ui.pad_x(20, ui.stack_y( + ui.pad_y(15, ui.center_x(box_title)), + elem, + ui.pad_y(20) + )) } +end + +local function mm_create(actions) + local host_game = menubox("Host Game", ui.pad_x(10, + ui.flex(1, ui.button("Create Save")), + ui.flex(1, ui.button("Load Save")) + )) + + local invite_code = ui.input("Invite Code") + local join_game = menubox("Join Game", ui.pad_x(10, + ui.flex(3, invite_code), + ui.flex(1, ui.button("Join", function() + if invite_code.input_value ~= "" then + actions.join_game(invite_code.input_value) + end + end)) + )) + + local edit_map = menubox("Edit Map", ui.button("Open Editor")) + + return ui.new(ui.stack_y( + ui.pad_y(50, ui.center_x(ui.text("RAINBOW SIX: PANOPTICON", 50, color(0x000000)))), + ui.stack_x( + ui.flex(1), + ui.flex(4, ui.pad_y(20, + host_game, + join_game, + edit_map + )), + ui.flex(1) + ) + )) +end + +return { + box = menubox, + create = mm_create, +} @@ -0,0 +1,412 @@ +local bit32 = require("bit32") +local utf8 = require("utf8") +local distribute = require("distribute") + +local function ui_new(root) + return { targets = {}, root = root } +end + +local function compute(x, ...) + if type(x) == "function" then + return x(...) + end + return x +end + +local function access(t, k) + if t ~= nil then + return t[k] + end +end + +local function align(kind, pos, needed, avail) + if not kind then + return pos, avail + elseif kind == "start" then + return pos, needed + elseif kind == "center" then + return pos+(avail-needed)/2, needed + elseif kind == "end" then + return pos+avail-needed, needed + end +end + +local function add_computed(node, attr, k, v) + node.computed = node.computed or {} + node.computed[attr] = node.computed[attr] or {} + node.computed[attr][k] = v +end + +local function node_compute_min_size(node, dir) + local size = access(compute(node.size, node), dir) + add_computed(node, "fixed_size", dir, size) + + if not size then + size = 0 + for _, child in ipairs(node) do + local child_size = node_compute_min_size(child, dir) + if node.stack == dir then + size = size + child_size + else + size = math.max(size, child_size) + end + end + end + + add_computed(node, "min_size", dir, size) + return size +end + +local function node_compute_layout(node, dir, pos, size) + size = node.computed.fixed_size[dir] or size + local align_as = access(node.align, dir) + + add_computed(node, "pos", dir, pos) + add_computed(node, "size", dir, size) + + if node.stack ~= dir then + for _, child in ipairs(node) do + node_compute_layout(child, dir, align(align_as, pos, child.computed.min_size[dir], size)) + end + return + end + + local min_size_noflex = 0 + local flex_elems = {} + local has_flex + for i, child in ipairs(node) do + if child.flex then + has_flex = true + flex_elems[i] = { + min = child.computed.min_size[dir], + weight = child.flex, + } + else + min_size_noflex = min_size_noflex + child.computed.min_size[dir] + end + end + + distribute(size-min_size_noflex, flex_elems) + + if not has_flex then + pos = align(align_as, pos, min_size_noflex, size) + end + + for i, child in ipairs(node) do + local child_size = access(flex_elems[i], "receive") or child.computed.min_size[dir] + node_compute_layout(child, dir, pos, child_size) + pos = pos + child_size + end +end + +local function tree_compute_layout(tree, x, y, width, height) + node_compute_min_size(tree, "x") + node_compute_min_size(tree, "y") + node_compute_layout(tree, "x", x, width) + node_compute_layout(tree, "y", y, height) +end + +-- render + +local function update_flags(node, targets, flags) + local new = {} + for k, v in pairs(targets) do + new[k] = node == v or (flags[k] and not (node.events and node.events[k])) + end + return new +end + +local function node_render(node, targets, flags) + flags = update_flags(node, targets, flags) + + local pos, size = node.computed.pos, node.computed.size + if node.fill then + love.graphics.setColor(compute(node.fill, node, flags)) + love.graphics.rectangle("fill", pos.x, pos.y, size.x, size.y) + end + if node.line then + love.graphics.setColor(compute(node.line, node, flags)) + love.graphics.rectangle("line", pos.x, pos.y, size.x, size.y) + end + if node.text then + love.graphics.setColor(compute(node.color, node, flags)) + love.graphics.draw(node.text, pos.x, pos.y) + end + + for _, child in ipairs(node) do + node_render(child, targets, flags) + end +end + +local function ui_render(ui) + node_render(ui.root, ui.targets, {}) +end + +-- events + +local function node_get_target(node, event, x, y) + -- todo: maybe reverse iteration? + for _, child in ipairs(node) do + local hov = node_get_target(child, event, x, y) + if hov then + return hov + end + end + + if not node.events or not node.events[event] then + return nil + end + + local pos, size = node.computed.pos, node.computed.size + if x >= pos.x and y >= pos.y and x < pos.x + size.x and y < pos.y + size.y then + return node + end +end + +local function ui_get_target(ui, event) + return node_get_target(ui.root, event, love.mouse.getPosition()) +end + +local function fire_event(node, event, ...) + if node and node.events and node.events[event] then + if type(node.events[event]) == "function" then + node.events[event](node, ...) + end + return true + end + return false +end + +local function ui_set_target(ui, event, elem) + local old = ui.targets[event] + if old ~= elem then + fire_event(old, event.."_end") + fire_event(elem, event) + end + ui.targets[event] = elem +end + +local function ui_update_target(ui, event) + ui_set_target(ui, event, ui_get_target(ui, event)) +end + +local function ui_mousepressed(ui, x, y, button) + if button ~= 1 then + return false + end + + ui_update_target(ui, "press") + ui_update_target(ui, "focus") + + local focus = ui.targets.focus + if focus and focus.events.input then + local pos, size = focus.computed.pos, focus.computed.size + love.keyboard.setTextInput(true, pos.x, pos.y, size.x, size.y) + else + love.keyboard.setTextInput(false) + end + + if ui.targets.press or ui.targets.focus then + return true + end + + return false +end + +local function ui_mousereleased(ui, x, y, button) + if button ~= 1 then + return false + end + + local press = ui_get_target(ui, "press", love.mouse.getPosition()) + if press == ui.targets.press then + fire_event(press, "click") + end + ui_set_target(ui, "press", nil) + + if press then + return true + end + return false +end + +local function ui_textinput(ui, text) + return fire_event(ui.targets.focus, "input", text) +end + +local function ui_keypressed(ui, key) + if key == "backspace" then + return fire_event(ui.targets.focus, "input", "\b") + elseif key == "v" and (love.keyboard.isDown("rctrl") or love.keyboard.isDown("lctrl")) then + return fire_event(ui.targets.focus, "input", love.system.getClipboardText()) + end + + return false +end + +-- update + +local function ui_update(ui) + tree_compute_layout(ui.root, 0, 0, love.graphics:getDimensions()) + ui_update_target(ui, "hover") +end + +-- presets + +local function color(hex) + return { love.math.colorFromBytes( + bit32.band(bit32.rshift(hex, 16), 0xff), + bit32.band(bit32.rshift(hex, 8), 0xff), + bit32.band(bit32.rshift(hex, 0), 0xff) + ) } +end + +local function ui_flex(n, ...) + return { flex = n, ... } +end + +local function pad(dir, size, ...) + local p = { size = { [dir] = size } } + local nodes = { ... } + if #nodes > 1 then + local x = { stack = dir } + for i, n in ipairs(nodes) do + table.insert(x, n) + if i < #nodes then + table.insert(x, p) + end + end + return x + elseif #nodes == 1 then + return { stack = dir, p, ui_flex(1, nodes[1]), p } + end + return p +end + +local function ui_pad_x(...) + return pad("x", ...) +end + +local function ui_pad_y(...) + return pad("y", ...) +end + +local function center_x(...) + return { align = { x = "center" }, ... } +end + +local function center_y(...) + return { align = { y = "center" }, ... } +end + +local function stack_x(...) + return { stack = "x", ... } +end + +local function stack_y(...) + return { stack = "y", ... } +end + +local function ui_text(str, size, col) + return { + color = col, + text = love.graphics.newText(love.graphics.newFont(size), str), + size = function(self) return { x = self.text:getWidth(), y = self.text:getHeight() } end, + } +end + +local function ui_button(label, click) + return { + fill = function(self, f) + return f.hover and color(0x8080a0) or color(0x808080) + 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)) + ))) + } +end + +local function ui_input(placeholder) + local function is_placeholder(self, focus) + return self.input_value == "" and not focus + end + local function update_text(self, focus) + self.input_text:set(is_placeholder(self, focus) and self.input_placeholder or self.input_value) + self.input_interact_time = love.timer.getTime() + end + + local text = ui_text(placeholder, 25, function(self, f) + return is_placeholder(self.input_parent, f.focus) and color(0xb0b0b0) or color(0x000000) + end) + + local marker = { + size = { x = 1, y = text.text:getHeight() }, + fill = function(self, f) + return (f.focus and + math.floor(love.timer.getTime() - self.input_parent.input_interact_time) % 2 == 0) + and color(0x000000) or color(0xffffff) + end + } + + -- TODO: cursor + -- TODO: overflow as scroll + -- TODO: paste icon (optional) + + local elem = { + fill = color(0xffffff), + line = function(self, f) + return f.focus and color(0x5050ff) or color(0x505050) + end, + events = { + focus = function(self) + update_text(self, true) + end, + focus_end = function(self) + update_text(self, false) + end, + input = function(self, text) + if text == "\b" then + local byteoffset = utf8.offset(self.input_value, -1) + if byteoffset then + self.input_value = string.sub(self.input_value, 1, byteoffset - 1) + end + else + self.input_value = self.input_value .. text + end + update_text(self, true) + end, + }, + input_value = "", + input_text = text.text, + input_placeholder = placeholder, + ui_pad_y(5, ui_pad_x(10, stack_x(text, marker))) + } + + text.input_parent = elem + marker.input_parent = elem + + return elem +end + +return { + new = ui_new, + render = ui_render, + update = ui_update, + keypressed = ui_keypressed, + textinput = ui_textinput, + mousepressed = ui_mousepressed, + mousereleased = ui_mousereleased, + color = color, + pad_x = ui_pad_x, + pad_y = ui_pad_y, + center_x = center_x, + center_y = center_y, + stack_x = stack_x, + stack_y = stack_y, + flex = ui_flex, + text = ui_text, + button = ui_button, + input = ui_input, +} |
