diff options
| author | Lizzy Fleckenstein <lizzy@vlhl.dev> | 2026-06-21 03:12:51 +0200 |
|---|---|---|
| committer | Lizzy Fleckenstein <lizzy@vlhl.dev> | 2026-06-21 03:12:51 +0200 |
| commit | 2ce0500f739cf68b7a5b475c6050ecdbf107fa93 (patch) | |
| tree | ed10a48d8289f56c4d0c4b35551a11b50c6ef327 | |
| parent | 0fa7e4e3c6b8989314759cb4c99c1bab262a431d (diff) | |
| download | r6p-2ce0500f739cf68b7a5b475c6050ecdbf107fa93.tar.xz | |
update lobby
| -rw-r--r-- | ATTRIBUTION | 6 | ||||
| -rw-r--r-- | assets/bg/rainbow_six_siege_x_clash.jpg | bin | 0 -> 748017 bytes | |||
| -rw-r--r-- | assets/font/DINEngschrift Regular.ttf | bin | 0 -> 40908 bytes | |||
| -rw-r--r-- | assets/icon/angle_left.png | bin | 0 -> 4100 bytes | |||
| -rw-r--r-- | assets/icon/angle_right.png | bin | 0 -> 3989 bytes | |||
| -rw-r--r-- | assets/icon/copy.png | bin | 0 -> 605 bytes | |||
| -rw-r--r-- | client.lua | 159 | ||||
| -rw-r--r-- | common.lua | 6 | ||||
| -rw-r--r-- | conf.lua | 3 | ||||
| -rw-r--r-- | dialog.lua | 50 | ||||
| -rw-r--r-- | lang.lua | 27 | ||||
| -rw-r--r-- | lang/en.lua | 88 | ||||
| -rw-r--r-- | lobby.lua | 272 | ||||
| -rw-r--r-- | main.lua | 367 | ||||
| -rw-r--r-- | main_menu.lua | 279 | ||||
| -rw-r--r-- | save_file.lua | 4 | ||||
| -rw-r--r-- | schema.lua | 77 | ||||
| -rw-r--r-- | server.lua | 321 | ||||
| -rwxr-xr-x | test_client.lua | 33 | ||||
| -rw-r--r-- | ui.lua | 538 | ||||
| -rw-r--r-- | util.lua | 96 |
21 files changed, 1637 insertions, 689 deletions
diff --git a/ATTRIBUTION b/ATTRIBUTION new file mode 100644 index 0000000..577fad7 --- /dev/null +++ b/ATTRIBUTION @@ -0,0 +1,6 @@ +assets/icon/copy.png: https://commons.wikimedia.org/wiki/File:Font_Awesome_5_regular_copy.svg + magick -background none Font_Awesome_5_regular_copy.svg -scale 25x25 copy.png +assets/icon/angle_left.png: https://commons.wikimedia.org/wiki/File:Font_Awesome_5_solid_angle-left.svg + magick -background none Font_Awesome_5_solid_angle-left.svg xc:white -channel RGB -clut angle_left.png +assets/icon/angle_right.png: https://commons.wikimedia.org/wiki/File:Font_Awesome_5_solid_angle-right.svg + magick -background none Font_Awesome_5_solid_angle-right.svg xc:white -channel RGB -clut angle_right.png diff --git a/assets/bg/rainbow_six_siege_x_clash.jpg b/assets/bg/rainbow_six_siege_x_clash.jpg Binary files differnew file mode 100644 index 0000000..3ef22ea --- /dev/null +++ b/assets/bg/rainbow_six_siege_x_clash.jpg diff --git a/assets/font/DINEngschrift Regular.ttf b/assets/font/DINEngschrift Regular.ttf Binary files differnew file mode 100644 index 0000000..5e0476b --- /dev/null +++ b/assets/font/DINEngschrift Regular.ttf diff --git a/assets/icon/angle_left.png b/assets/icon/angle_left.png Binary files differnew file mode 100644 index 0000000..7821eb0 --- /dev/null +++ b/assets/icon/angle_left.png diff --git a/assets/icon/angle_right.png b/assets/icon/angle_right.png Binary files differnew file mode 100644 index 0000000..4995daa --- /dev/null +++ b/assets/icon/angle_right.png diff --git a/assets/icon/copy.png b/assets/icon/copy.png Binary files differnew file mode 100644 index 0000000..dc94acc --- /dev/null +++ b/assets/icon/copy.png @@ -16,9 +16,26 @@ local function log_peer(clt, peer, msg) log(peer_info(clt, peer).name .. ": " .. msg) end -local function create_client(secret) +local function push_event(clt, type, evt) + evt = evt or {} + evt.type = type + table.insert(clt.events, evt) +end + +local function push_error(clt, error, fatal) + push_event(clt, "error", { error = error, fatal = fatal }) +end + +local function push_wait(clt, what) + push_event(clt, "wait", { what = what }) +end + +local function create_client(player, secret) + assert(player) local clt = {} clt.host = enet.host_create() + clt.events = {} + clt.player = player clt.secret = secret return clt end @@ -29,17 +46,10 @@ local function connect(clt, addrs) clt.server_candidates[clt.host:connect(addr)] = true end clt.server_req = socket.gettime() - clt.status = "wait_server" + push_wait(clt, "server") end -local function remove_candidates(clt) - for s in pairs(clt.server_candidates) do - s:disconnect() - end - clt.server_candidates = nil -end - -function client.join(invite, match_addr) +function client.join(player, invite, match_addr) local invite_dec = util.base64_dec(invite) if not invite_dec then return nil, "invalid_invite" @@ -48,16 +58,16 @@ function client.join(invite, match_addr) local game_id = invite_dec:sub(1, common.gameid_len) local secret = invite_dec:sub(common.gameid_len+1) - local clt = create_client(secret) + local clt = create_client(player, secret) + clt.game_id = game_id clt.match = clt.host:connect(match_addr or common.default_match_addr) clt.match_req = socket.gettime() - clt.game_id = game_id - clt.status = "wait_match" + push_wait(clt, "match") return clt end -function client.connect(addr, secret) - local clt = create_client(secret) +function client.connect(player, addr, secret) + local clt = create_client(player, secret) connect(clt, { addr }) return clt end @@ -65,50 +75,65 @@ end local schema_match = schema.parse("client_match_pkt", [[ union { client_join: { candidates: [string] }, - client_join_fail: {}, + client_join_fail, } ]]) local function handle_match(clt, pkt) - if clt.status ~= "wait_match" then - log_peer(clt, clt.match, pkt.type .. " illegal while not waiting") - return - end - if pkt.type == "client_join" then connect(clt, pkt.candidates) elseif pkt.type == "client_join_fail" then - clt.status = "fail_match" + push_error(clt, "fail_match", true) end + clt.match_req = nil clt.match:disconnect() clt.match = nil end local schema_server = schema.parse("client_pkt", [[ union { - client_hi: {}, - client_reject: {}, + client_kick: { + reason: string, + }, + client_content: { + factions: [{ + id: string, + name: string, + }], + }, client_info: { + lobby: boolean, + started: boolean, players: [{ name: string, + uuid: string, active: boolean, - }] + ready: boolean, + team: ?$team, + faction: ?string, + }], }, - client_player: { name: string }, - client_player_fail: { error: string }, + client_countdown: { time: ?number }, } -]]) +]], common.dict) local function handle_server(clt, pkt) - if pkt.type == "client_hi" then - clt.status = "active" - elseif pkt.type == "client_reject" then - clt.status = "fail_server" + if pkt.type == "client_kick" then + push_error(clt, "kick."..pkt.reason, true) + elseif pkt.type == "client_content" then + clt.content = pkt + clt.factions_by_id = util.index_by(clt.content.factions, "id") elseif pkt.type == "client_info" then + if not clt.content then + push_error(clt, "unexpected", true) + end 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 + push_event(clt, "update_info") + clt.player = util.find_by(clt.info.players, "uuid", clt.player.uuid) + if not clt.player then + push_error(clt, "unexpected", true) + end + elseif pkt.type == "client_countdown" then + push_event(clt, "countdown", { time = pkt.time }) end end @@ -124,9 +149,23 @@ peer_info = function(clt, peer) end end +function client.select(clt, what, val) + util.send(clt.server, { type = "server_select", item = { type = what, [what] = val } }) +end + function client.update(clt) + if clt.match_req and clt.match_req+3 < socket.gettime() then + push_error(clt, "timeout_match", true) + clt.match_req = nil + end + + if clt.server_req and clt.server_req+5 < socket.gettime() then + push_error(clt, "timeout_server", true) + clt.server_req = nil + end + local event = clt.host:service() - while event do + while event and clt.host do if event.type == "receive" then local info = peer_info(clt, event.peer) local pkt, err = schema.deserialize(info.schema, event.data) @@ -137,50 +176,42 @@ function client.update(clt) end elseif event.type == "connect" then log_peer(clt, event.peer, "connect") - if clt.status == "wait_match" and event.peer == clt.match then + if event.peer == clt.match then util.send(clt.match, { type = "match_join", game_id = util.base64_enc(clt.game_id) }) - elseif clt.status == "wait_server" and clt.server_candidates[event.peer] then + elseif clt.server_candidates and clt.server_candidates[event.peer] then clt.server = event.peer clt.server_candidates[clt.server] = nil - util.send(clt.server, { type = "server_hi", secret = util.base64_enc(clt.secret) }) - remove_candidates(clt) + util.send(clt.server, { + type = "server_hi", + version = common.proto_version, + secret = util.base64_enc(clt.secret), + player = clt.player, + }) + for s in pairs(clt.server_candidates) do + s:disconnect() + end + clt.server_candidates = nil + clt.server_req = nil + push_wait(clt, "lobby") else event.peer:disconnect_now() end elseif event.type == "disconnect" then log_peer(clt, event.peer, "disconnect") if event.peer == clt.server then - clt.status = "disco" + push_error(clt, "disconnected", true) end end event = clt.host:service() end -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.server_req+5 < socket.gettime() then - clt.status = "timeout_server" - end - - return clt.status -end -function client.select_player(clt, name, create) - util.send(clt.server, { - type = "server_player", - name = name, - create = create, - }) + local events = clt.events + clt.events = {} + return events end function client.close(clt) - if clt.match then clt.match:disconnect() end - if clt.server then clt.server:disconnect() end - if clt.server_candidates then remove_candidates(clt) end - clt.host:flush() - clt.host:destroy() + util.close_host(clt.host) end return client @@ -1,3 +1,5 @@ +local schema = require("schema") + return { default_match_addr = "ivy.vlhl.dev:18252", -- any public IP address (in this case: ivy.vlhl.dev) @@ -6,4 +8,8 @@ return { route_lookup_target = "94.16.117.28", gameid_len = 8, secret_len = 4, + proto_version = 1, + dict = schema.parse_dict( + "team", "?enum[attack, defend, third_party]" + ) } diff --git a/conf.lua b/conf.lua deleted file mode 100644 index d094913..0000000 --- a/conf.lua +++ /dev/null @@ -1,3 +0,0 @@ -function love.conf(t) - t.window.resizable = true -end diff --git a/dialog.lua b/dialog.lua new file mode 100644 index 0000000..28435f1 --- /dev/null +++ b/dialog.lua @@ -0,0 +1,50 @@ +local ui = require("ui") +local L = require("lang").get + +local dialog = {} + +function dialog.show(dg, ...) + dialog.active = dg + if dg.show then + dg:show(...) + end +end + +function dialog.refresh() + if dialog.active then + dialog.show(dialog.active) + end +end + +local function dialog_loading(game) + return ui.popup_box(L("wait.generic"), L("dialog.cancel"), + function() game.quit() end, + function(self, msg) + if msg then + self.title.text:set(L("wait."..msg)) + end + end + ) +end + +local function dialog_error(game) + local back_menu + return ui.popup_box(L("error.generic"), L("dialog.back"), + function() + dialog.show(back_menu or game.main_menu) + end, + function(self, msg, back) + if msg then + self.title.text:set(L("error."..msg)) + back_menu = back + end + end + ) +end + +function dialog.init(game) + dialog.loading = dialog_loading(game) + dialog.error = dialog_error(game) +end + +return dialog diff --git a/lang.lua b/lang.lua new file mode 100644 index 0000000..6aa8b98 --- /dev/null +++ b/lang.lua @@ -0,0 +1,27 @@ +local en = require("lang.en") + +local function get(...) + local path + local node = en + local function f(...) + for _, comp in ipairs({...}) do + path = (path and (path .. ".") or "") .. comp + for x in comp:gmatch("[^%.]+") do + if type(node) == "table" then + node = node[x] + else + return + end + end + end + end + f(...) + if type(node) == "string" then + return node + else + return path or "" + end +end + +-- ipairs({...}) +return { get = get } diff --git a/lang/en.lua b/lang/en.lua new file mode 100644 index 0000000..e3250a3 --- /dev/null +++ b/lang/en.lua @@ -0,0 +1,88 @@ +return { + title = "RAINBOW SIX: PANOPTICON", + error = { + generic = "Error", + invalid_uuid = "Invalid UUID", + client = { + unexpected = "Unexpected Error", + invalid_invite = "Invalid invite", + timeout_match = "Failed to connect to match server (Match server down?)", + fail_match = "Game not found (Server shut down?)", + timeout_server = "Failed to connect to server (NAT punch failure?)", + kick = { + incompatible_version = "Incompatible protocol version (Own or host's game out of date?)", + incorrect_secret = "Incorrect secret (Invalid invite?)", + game_started = "Game already started", + name_too_long = "Player name is too long", + name_taken = "Player name already taken", + already_active = "Player already logged in", + no_such_player = "Player is not part of this game", + }, + disconnected = "Lost connection to server (Server shut down?)", + }, + server = { + fail_match = "Failed to register match (Match server down?)" + }, + save = { + exists = "Savefile with this name already exists", + corrupted = "Savefile corrupted (Try restoring backup)", + failed_write = "Failed to write to savefile (Missing permissions?)", + } + }, + wait = { + generic = "Loading...", + server_match = "Registering game...", + match = "Waiting for match server...", + server = "Waiting for server...", + lobby = "Joining lobby...", + }, + dialog = { + back = "Back", + cancel = "Cancel", + main_menu = { + player = { + title = "Player Profile", + name = "Player name", + uuid = "UUID", + edit = "Edit", + }, + edit_player = { + title = { + edit = "Edit Player", + create = "Create Player", + }, + submit = { + edit = "Save", + create = "Create", + }, + }, + host_game = "Host Game", + join_game = { + title = "Join Game", + invite = "Invite Code", + button = "Join", + }, + load_save = "Load Save", + create_save = { + title = "Create Save", + button = "Create", + name = "Save name" + }, + edit_map = { + title = "Edit Map", + button = "Open Editor", + }, + }, + lobby = { + invite = "INVITE CODE: ", + invite_reveal = "reveal", + ready = "READY?", + empty_slot = "empty slot", + team = "//TEAM", + faction = "//FACTION", + players = "CONNECTED PLAYERS", + countdown = "Continuing in", + }, + }, +} + diff --git a/lobby.lua b/lobby.lua new file mode 100644 index 0000000..0cfbc6e --- /dev/null +++ b/lobby.lua @@ -0,0 +1,272 @@ +local client = require("client") +local util = require("util") +local ui = require("ui") +local dialog = require("dialog") +local socket = require("socket") +local L = require("lang").get +local C = ui.color + +local teams = { + { id = "attack", name = "ATK", col = C(0xEF8131) }, + { id = "defend", name = "DEF", col = C(0x6F8AAC) }, + { id = "third_party", name = "PO3", col = C(0xF1E816) }, +} + +local color = { + greyed = C(0x606060), + hover = C(0x404040), + normal = C(0x000000), + me = C(0x000000), +} + +local function make_selection(lobby, key, dir) + local list = key == "team" and teams or lobby.game.client.content[key.."s"] + local current = lobby.game.client.player[key] + local index + + if current then + index = util.find_index_by(list, "id", current) + elseif not current then + index = 1 + if dir == 1 then dir = 0 end + end + + index = (index-1+dir)%#list+1 + client.select(lobby.game.client, key, list[index].id) +end + +local function insert_row(columns, elems, cell) + cell = cell or function(x) return x end + local size = 0 + for i = 1, #columns do + if elems[i] then + size = math.max(size, ui.min_size(elems[i], "y")) + end + end + for i = 1, #columns do + table.insert(columns[i], cell({ { size = { y = size } }, elems[i] })) + end +end + +local function clear_table(t) + while #t > 0 do + table.remove(t) + end +end + +local function invite_display(lobby) + local copy_btn = ui.copy_button(function() + return lobby.game.server.invite + end, 25) + local reveal_btn = ui.click(ui.center_x(ui.text(L("dialog.lobby.invite_reveal"), 30, C(0x000000))), + function(self) self.active = false end) + + for _, b in ipairs({ reveal_btn, copy_btn }) do + b.line = C(0x000000) + b.line_width = 4 + b.fill_hover = C(0x8080a0) + b.fill_normal = C(0xEFE3AF) + end + + local code = ui.text(code, 30, color.normal) + + local elem = ui.align_right(ui.stack_x( + ui.center_y(ui.text(L("dialog.lobby.invite"), 30, color.normal)), + { code, reveal_btn }, + ui.pad_x(8), + copy_btn + )) + + function elem:show() + if lobby.game.server then + code.text:set(lobby.game.server.invite) + else + elem.active = false + end + end + + return elem +end + +local function text(x, col) + return ui.text(x, 30, col or C(0x000000)) +end + +local function fill_player_columns(lobby, columns) + local info = lobby.game.client.info + local me = lobby.game.client.player + local teams_by_id = util.index_by(teams, "id") + + for _, c in ipairs(columns) do + clear_table(c) + end + + local function cell(x) + return { line = C(0x000000), line_width = 4, line_segments = { 1, 3 }, ui.pad_x(10, ui.pad_y(10, x)) } + end + + local function player_name(name, me, active) + return text(name, (me and color.me) or (active and color.normal) or color.greyed) + end + + for _, p in ipairs(info.players) do + local name_cell = player_name(p.name, p == me, p.active) + local team_cell = p.team and ui.align_left({ fill = teams_by_id[p.team].col, text(teams_by_id[p.team].name, C(0x000000)) }) or {} + local faction_cell = p.faction and text(lobby.game.client.factions_by_id[p.faction].name) or {} + + local ready = text(L("dialog.lobby.ready")) + local ready_size = ready.text:getHeight() + local ready_elem = ui.pad_x(5, ready, { + fill = p.ready and C(0x1EB251) or C(0xEF1B22), + line = C(0x000000), + line_width = 4, + size = { x = ready_size, y = ready_size }, + }) + if p == me then + ready_elem = ui.click(ready_elem, function() + client.select(lobby.game.client, "ready", not lobby.game.client.player.ready) + end) + end + local ready_cell = ui.align_right(ready_elem) + + insert_row(columns, { name_cell, team_cell, faction_cell, ready_cell }, cell) + end + + if not info.started then + insert_row(columns, { player_name(L("dialog.lobby.empty_slot")) }, cell) + end +end + +local function player_display(lobby) + local columns = ui.stack_x() + for i = 1, 4 do + local column = ui.stack_y() + column.flex = ({ 2, nil, nil, 3 })[i] + table.insert(columns, column) + end + + local player_columns = {} + for i = 1, #columns do + local angle = -75/180*math.pi + local column = ui.stack_y() + column.line = C(0x000000) + column.line_width = 4 + column.angle = { x = { i ~= 1 and angle, i ~= #columns and angle } } + column.flex = 1 + column.debug = "col_"..i + table.insert(player_columns, column) + end + + local button_image = {} + + local function header(id) + local t = text(L("dialog.lobby", id)) + + local function vbtn(sym, dir) + local image = ui.image("assets/icon/angle_"..sym..".png", "scale", { y = t.text:getHeight() }) + image.color_hover = color.hover + image.color_normal = color.normal + table.insert(button_image, image) + return ui.click(image, function() + if not lobby.game.client.info.started then + make_selection(lobby, id, dir) + end + end) + end + + return ui.pad_x(2, ui.stack_x(vbtn("left", -1), ui.flex(1, ui.center_x(t)), vbtn("right", 1))) + end + + insert_row(columns, { nil, header("team"), header("faction"), nil }) + insert_row(columns, player_columns) + + function columns:show() + fill_player_columns(lobby, player_columns) + if lobby.game.client.info.started then + for _, img in ipairs(button_image) do + img.color = color.greyed + end + end + end + + return columns +end + +local function countdown_display(lobby) + local text = ui.text("", 40, C(0x000000)) + function text:update() + if lobby.countdown_time then + text.visible = true + local secs = math.max(0, math.ceil(lobby.countdown_time - socket.gettime())) + if secs ~= lobby.countdown_secs then + text.text:set(L("dialog.lobby.countdown") .. " " .. secs .. "...") + lobby.countdown_secs = secs + end + if secs == 0 then + lobby.countdown_time = nil + end + else + text.visible = false + end + end + return ui.align_right(ui.align_top(ui.pad_x(10, ui.pad_y(10, text)))) +end + +local function dialog_lobby(lobby) + local invite = invite_display(lobby) + local players = player_display(lobby) + local countdown = countdown_display(lobby) + + local box = ui.gradient(135, 0.3, { C(0x0060AA), C(0x0060AA), C(0xDFDB4B), C(0xB36910), C(0xB36910) }, { + line = C(0x000000), + line_width = 4, + ui.stack_x(ui.flex(1, ui.stack_y(ui.pad_y(10), invite, players)), ui.pad_x(15)) }) + + local dg = ui.new({ + ui.image("assets/bg/rainbow_six_siege_x_clash.jpg", "cover"), + ui.stack_x( + ui.flex(1), + ui.flex(1, ui.stack_y( + ui.flex(1), + ui.center_x(ui.text(L("dialog.lobby.players"), 150, C(0x000000))), + ui.flex(2, box) + )), + ui.flex(1) + ), + countdown + }) + + function dg:show() + invite:show() + players:show() + end + + return dg +end + + +local function show(lobby) + dialog.show(lobby.ui) +end + +local function set_countdown(lobby, time) + if time then + lobby.countdown_time = socket.gettime()+time + else + lobby.countdown_time = nil + end +end + +local function create(game) + local lobby = { game = game } + + lobby.ui = dialog_lobby(lobby) + + lobby.show = show + lobby.countdown = set_countdown + return lobby +end + +return { + create = create, +} @@ -19,342 +19,95 @@ local client = require("client") local server = require("server") local main_menu = require("main_menu") local ui = require("ui") -local color = ui.color +local dialog = require("dialog") +local lobby = require("lobby") +local lang = require("lang") -local clt -local dialog = {} -local last_status, last_info +local game = {} -local function show(dg, ...) - dialog.active = dg - if dg.show then - dg:show(...) +function game.quit(msg) + if game.client then + client.close(game.client) + game.client = nil end -end - -local function quit_game(msg) - if clt then - client.close(clt) - clt = nil - last_status = nil - end - if srv then - server.close(srv) - srv = nil + if game.server then + server.close(game.server) + game.server = nil end if msg then - show(dialog.error, msg) + dialog.show(dialog.error, msg) else - show(dialog.main_menu, msg) + dialog.show(game.main_menu) end end -local function save_location(save) - return love.filesystem.getSaveDirectory().."/saves/"..save..".json" -end - -local function client_join(invite) - local c, err = client.join(invite) +function game.join(player, invite) + local clt, err = client.join(player, invite) if err then - quit_game("Invalid invite") + game.quit("client.invalid_invite") else - clt = c + game.client = clt end end -local function client_connect(addr, secret) - clt = client.connect(addr, secret) +local function save_location(save) + return love.filesystem.getSaveDirectory().."/saves/"..save..".json" end -local function start_server(save) +function game.host(player, save) + game.player = player local s, err = server.create(save_location(save)) if err then - quit_game(err) -- TODO: better message + game.quit(err) else - srv = s + game.server = s end - show(dialog.loading, "Registering game...") -end - -local function create_save(save) - if love.filesystem.getInfo("saves/"..save..".json") then - show(dialog.error, "Savefile with this name already exists", dialog.create_save) - return - end - love.filesystem.createDirectory("saves") - start_server(save) -end - -local function select_player(name, create) - client.select_player(clt, name, create) - show(dialog.loading, "Joining Lobby...") -end - -local function show_create_save() - show(dialog.create_save) -end - -local function show_load_save() - show(dialog.load_save) + dialog.show(dialog.loading, "server_match") end local function update_client() - client.update(clt) - - local status = client.status(clt) - if status == "timeout_match" then - quit_game("Failed to connect to match server (match server down?)") - elseif status == "fail_match" then - quit_game("Game not found (invalid invite?)") - elseif status == "timeout_server" then - quit_game("Failed to connect to server (NAT punch failure?)") - elseif status == "fail_server" then - quit_game("Incorrect secret (invalid invite?)") - elseif status == "disco" then - quit_game("Lost connection to server (Server shut down?)") - elseif status ~= last_status then - if status == "active" then - show(dialog.loading, "Waiting for info...") - elseif status == "wait_match" then - show(dialog.loading, "Waiting for match server...") - elseif status == "wait_server" then - 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) -- TODO: better message - clt.player_error = nil - elseif clt.player and (dialog.active ~= dialog.lobby or 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 - -local function update_server() - server.update(srv, 0) - local status, invite = server.match_status(srv) - if status == "fail" then - quit_game("Failed to register match (match server down?)") - elseif status == "active" and not clt then - client_connect("localhost:" .. server.port(srv), srv.secret) - end -end - -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 - -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 function create_loading() - return create_info_box("Loading...", "Cancel", - function() quit_game() end, - function(self, msg) - self:set_title(msg) - end - ) -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 - ) -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 - - 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) + for _, event in ipairs(client.update(game.client)) do + if event.type == "error" then + if event.fatal then + game.quit("client."..event.error) else - button.fill_normal = color(0x223ebd) - button.fill_hover = color(0x2d95d6) + dialog.show(dialog.error, "client."..event.error, dialog.active) 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 invite = {} - local player_list = {} - local dg = create_dialog_box("Lobby", ui.pad_y(10, - invite, - player_list, - ui.center_x(ui.button("Disconnect", function() - quit_game() - end)) - ), 3) - function dg.show(self) - if srv then - local copied = {} - invite[1] = ui.pad_x(10, - ui.center_y(ui.text("Invite: " .. srv.invite, 25, color(0x000000))), - ui.button("Copy", function() - love.system.setClipboardText(srv.invite) - local ts = love.timer.getTime() - copied[1] = ui.text("Copied", 25, function() - local col = color(0x000000) - col[4] = math.max(0, ts+1-love.timer.getTime()) - return col - end) - end), - ui.center_y(copied)) - end - 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 - -local function back_button() - return ui.center_x(ui.button("Back", function() - show(dialog.main_menu) - end)) -end - -local function create_load_save() - local contents = {} - local dg = create_dialog_box("Load Save", ui.pad_y(15, contents, back_button())) - function dg.show(self) - local stack = ui.stack_y() - local ext = ".json" - local items = love.filesystem.getDirectoryItems("saves") - for _, item in ipairs(items) do - if item:sub(-#ext) == ext then - local name = item:sub(1, -#ext-1) - table.insert(stack, ui.button(name, function() - start_server(name) - end)) + elseif event.type == "wait" then + dialog.show(dialog.loading, event.what) + elseif event.type == "update_info" then + if game.client.info.lobby then + game.lobby:show() + else + dialog.show(dialog.loading, "todo") end + elseif event.type == "countdown" then + game.lobby:countdown(event.time) end - - table.insert(stack, ui.button("+", function() - show_create_save() - end)) - - contents[1] = stack end - return dg end -local function create_create_save() - local input_name = ui.input("Save name") - return create_dialog_box("Create Save", ui.pad_y(15, - ui.pad_x(10, - ui.flex(3, input_name), - ui.flex(1, ui.button("Create", function() - if input_name.input_value ~= "" then - create_save(input_name.input_value) - end - end)) - ), - back_button() - )) +local function update_server() + server.update(game.server, 0) + local status = server.match_status(game.server) + if status == "fail" then + game.quit("server.fail_match") + elseif status == "active" and not game.client then + game.client = client.connect(game.player, "localhost:" .. server.port(game.server), game.server.secret) + end end function love.load() - love.graphics.setBackgroundColor(color(0xffffff)) - love.window.setTitle("RAINBOW SIX: PANOPTICON") + love.graphics.setBackgroundColor(1, 1, 1) + love.window.setTitle(lang.get("title")) + local w, h = love.window.getMode() + love.window.setMode(w, h, { resizable = true, vsync = 0, minwidth = 1200, minheight = 900}) - dialog.main_menu = main_menu.create({ join_game = client_join, load_save = show_load_save, create_save = show_create_save }) - dialog.loading = create_loading() - dialog.error = create_error() - dialog.create_player = create_create_player() - dialog.select_player = create_select_player() - dialog.lobby = create_lobby() - dialog.load_save = create_load_save() - dialog.create_save = create_create_save() + dialog.init(game) + game.main_menu = main_menu.create(game) + game.lobby = lobby.create(game) - show(dialog.main_menu) + dialog.show(game.main_menu) end function love.mousepressed(x, y, button) @@ -382,10 +135,10 @@ function love.keypressed(key) end function love.update() - if clt then + if game.client then update_client() end - if srv then + if game.server then update_server() end end @@ -399,10 +152,10 @@ end function love.quit() print("shutting down") - if clt then - client.close(clt) + if game.client then + client.close(game.client) end - if srv then - server.close(srv) + if game.server then + server.close(game.server) end end diff --git a/main_menu.lua b/main_menu.lua index e84c673..f9cdada 100644 --- a/main_menu.lua +++ b/main_menu.lua @@ -1,48 +1,277 @@ local ui = require("ui") -local color = ui.color +local dialog = require("dialog") +local lang = require("lang") +local util = require("util") +local C = 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) - )) } +local function L(...) + return lang.get("dialog.main_menu", ...) end -local function mm_create(actions) - local host_game = menubox("Host Game", ui.pad_x(10, - ui.flex(1, ui.button("Create Save", actions.create_save)), - ui.flex(1, ui.button("Load Save", actions.load_save)) +local function current_player(mm) + return mm.players.players[mm.players.selected] +end + +local function load_players(mm) + local data = love.filesystem.read("players.json") + if data then + local players = util.json_dec(data) + if mm.players then + local sel_name = current_player(mm).name + for i, p in ipairs(players.players) do + if p.name == sel_name then + players.selected = i + break + end + end + end + mm.players = players + end +end + +local function save_players(mm) + love.filesystem.write("players.json", util.json_enc(mm.players)) +end + +local function create_save(mm, save) + if love.filesystem.getInfo("saves/"..save..".json") then + dialog.show(dialog.error, "save.exists", mm.create_save) + return + end + love.filesystem.createDirectory("saves") + mm.game.host(save) +end + +local function edit_player(mm, mode, name, uuid) + if not util.validate_uuidv4(uuid) then + dialog.show(dialog.error, "invalid_uuid", mm.edit_player) + return + end + + load_players(mm) + + if mode == "create" then + mm.players = mm.players or { players = {} } + table.insert(mm.players.players, {}) + mm.players.selected = #mm.players.players + end + + current_player(mm).name = name + current_player(mm).uuid = uuid + save_players(mm) + dialog.show(mm.main) +end + +local function select_player(mm, i) + mm.players.selected = i + load_players(mm) + save_players(mm) + dialog.show(mm.main) +end + +local function back_button(mm) + return ui.center_x(ui.button(lang.get("dialog.back"), function() + dialog.show(mm.main) + end)) +end + +local function dialog_load_save(mm) + local contents = {} + local dg = ui.dialog_box(L("load_save"), ui.pad_y(15, contents, back_button(mm))) + function dg.show(self) + local stack = ui.stack_y() + local ext = ".json" + local items = love.filesystem.getDirectoryItems("saves") + for _, item in ipairs(items) do + if item:sub(-#ext) == ext then + local name = item:sub(1, -#ext-1) + table.insert(stack, ui.button(name, function() + mm.game.host(current_player(mm), name) + end)) + end + end + + table.insert(stack, ui.button("+", function() + dialog.show(mm.create_save) + end)) + + contents[1] = stack + end + return dg +end + +local function dialog_create_save(mm) + local input_name = ui.input(L("create_save.name")) + return ui.dialog_box(L("create_save.title"), ui.pad_y(15, + ui.pad_x(10, + ui.flex(3, input_name), + ui.flex(1, ui.button(L("create_save.button"), function() + if input_name.value ~= "" then + create_save(input_name.value) + end + end)) + ), + back_button(mm) )) +end - 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) +local function dialog_edit_player(mm) + local input = {} + + local function row(what) + local text = L("player", what) + input[what] = ui.input(text, 25) + return ui.pad_x(10, ui.center_y(ui.text(text .. ":", 25, C(0x000000))), ui.flex(1, input[what])) + end + + local back = ui.flex(1, ui.stack_x(ui.flex(1, ui.button(lang.get("dialog.cancel"), function() + dialog.show(mm.main) + end)), ui.pad_x(10))) + local mode + + local submit = ui.button("", function() + local name, uuid = input.name.value, input.uuid.value + if name ~= "" and uuid ~= "" then + edit_player(mm, mode, name, uuid) + end + end) + local dg = ui.dialog_box("", ui.pad_y(10, + row("name"), + row("uuid"), + ui.stack_x(back, ui.flex(1, submit)) + )) + + function dg:show(set_mode) + back.active = mm.players ~= nil + if not set_mode then + return + end + + mode = set_mode + dg.title.text:set(L("edit_player.title", mode)) + submit.label.text:set(L("edit_player.submit", mode)) + + if set_mode == "create" then + input.name:set_value("") + input.uuid:set_value(util.gen_uuidv4()) + else + input.name:set_value(current_player(mm).name) + input.uuid:set_value(current_player(mm).uuid) + end + end + + return dg +end + +local function box_player_profile(mm) + local box = {} + local function row(what) + local text = L("player", what) + box[what] = ui.text(text, 25, C(0x000000)) + return ui.pad_x(10, ui.text(text .. ":", 25, C(0x000000)), box[what]) + end + + local function is_active(dir) + return mm.players.players[mm.players.selected+dir] ~= nil + end + + local function vbtn(sym, dir) + local image = ui.image("assets/icon/angle_"..sym..".png", "scale", { x = 25 }) + image.color = C(0x000000) + local btn = ui.button_elem(ui.center_y(image), function() + if is_active(dir) then + select_player(mm, mm.players.selected + dir) end + end) + function btn:fill(f) + return is_active(dir) and ui.modif_attr("fill")(self, f) or C(0x505050) + end + return ui.stack_y(ui.flex(1, btn)) + end + + box.ui = ui.box(L("player.title"), ui.pad_y(0, + ui.pad_x(10, + vbtn("left", -1), + ui.flex(1, { line = C(0x606060), ui.pad_x(10, ui.pad_y(10, ui.pad_y(10, + row("name"), + row("uuid"), + ui.button(L("player.edit"), function() + dialog.show(mm.edit_player, "edit") + end) + )))}), + vbtn("right", 1) + ) + )) + + table.insert(box.ui.inner, ui.align_right(ui.align_top(ui.button("+", function() + dialog.show(mm.edit_player, "create") + end)))) + return box +end + +local function dialog_main(mm) + local player_profile = box_player_profile(mm) + + local host_game = ui.box(L("host_game"), ui.pad_x(10, + ui.flex(1, ui.button(L("create_save.title"), function() + dialog.show(mm.create_save) + end)), + ui.flex(1, ui.button(L("load_save"), function() + dialog.show(mm.load_save) end)) )) - local edit_map = menubox("Edit Map", ui.button("Open Editor")) + local invite_code = ui.input(L("join_game.invite")) + local join_game = ui.box(L("join_game.title"), ui.pad_x(10, + ui.flex(3, invite_code), + ui.flex(1, ui.button(L("join_game.button"), function() + if invite_code.value ~= "" then + mm.game.join(current_player(mm), invite_code.value) + end + end)) + )) - return ui.new(ui.stack_y( - ui.pad_y(50, ui.center_x(ui.text("RAINBOW SIX: PANOPTICON", 50, color(0x000000)))), + local edit_map = ui.box(L("edit_map.title"), ui.button(L("edit_map.button"))) + local dg = ui.new({ ui.image("assets/bg/rainbow_six_siege_x_clash.jpg", "cover"), ui.stack_y( + ui.pad_y(50, ui.center_x(ui.text(lang.get("title"), 100, C(0x000000)))), ui.stack_x( ui.flex(1), ui.flex(4, ui.pad_y(20, host_game, join_game, - edit_map + edit_map, + player_profile.ui )), ui.flex(1) ) - )) + )}) + + function dg:show() + if not mm.players then + dialog.show(mm.edit_player, "create") + return + end + + player_profile.name.text:set(current_player(mm).name) + player_profile.uuid.text:set(current_player(mm).uuid) + end + + return dg +end + +local function create(game) + local mm = { game = game } + + load_players(mm) + + mm.main = dialog_main(mm) + mm.create_save = dialog_create_save(mm) + mm.load_save = dialog_load_save(mm) + mm.edit_player = dialog_edit_player(mm) + + return mm.main end return { - box = menubox, - create = mm_create, + create = create, } diff --git a/save_file.lua b/save_file.lua index f3559d4..1ad6385 100644 --- a/save_file.lua +++ b/save_file.lua @@ -23,14 +23,14 @@ local function save_file_read(filename) data = util.json_dec(f:read("*all")) f:close() if not data then - return nil, "save_corrupted" + return nil, "save.corrupted" end log("loaded " .. filename) else data = {} end if not save_file_write(filename, data) then - return nil, "save_failed_write" + return nil, "save.failed_write" end return data end @@ -8,6 +8,7 @@ local util = require("util") -- array[key] OR [key] -- struct { hi : string, bye: blah } OR { hi: string, bye: blah }, {} -- map[key, value] +-- set[key] -- enum[a,b,c,d] -- union { foo: array[string] } @@ -44,6 +45,14 @@ end local parse_type +local function expect_ident(stream) + local ident = stream_ident(stream) + if not ident then + return nil, "expected identifier, got " .. (stream_any(stream) or "EOF") + end + return ident +end + local function expect_char(stream, chars) local c = stream_char(stream, chars) if c then @@ -102,6 +111,8 @@ parse_type = function(stream) return { kind = "integer", unsigned = ident == "uint" } elseif ident == "base64" then return { kind = "base64" } + elseif ident == "uuidv4" then + return { kind = "uuidv4" } elseif ident == "option" then local _, err = expect_char(stream, "[") if err then return nil, err end local inner, err = expect_type(stream) if err then return nil, err end @@ -121,6 +132,11 @@ parse_type = function(stream) local value, err = expect_type(stream) if err then return nil, err end local _, err = expect_char(stream, "]") if err then return nil, err end return { kind = "map", key = key, value = value } + elseif ident == "set" then + local _, err = expect_char(stream, "[") if err then return nil, err end + local inner, err = expect_type(stream) if err then return nil, err end + local _, err = expect_char(stream, "]") if err then return nil, err end + return { kind = "set", inner = inner } elseif ident == "enum" then local _, err = expect_char(stream, "[") if err then return nil, err end local variants = {} @@ -147,8 +163,13 @@ parse_type = function(stream) if not ident then break end - local _, err = expect_char(stream, ":") if err then return nil, err end - local variant, err = expect_type(stream) if err then return nil, err end + local variant + if stream_char(stream, ":") then + local err + variant, err = expect_type(stream) if err then return nil, err end + else + variant = { kind = "struct", fields = {} } + end table.insert(strings, ident) variants[ident] = variant if not stream_char(stream, ",") then @@ -161,7 +182,7 @@ parse_type = function(stream) return nil, "expected type, got " .. ident end end - local char, err = expect_char(stream, "{[?") if err then return nil, err end + local char, err = expect_char(stream, "?[{$") if err then return nil, err end if char == "?" then local inner, err = expect_type(stream) if err then return nil, err end return { kind = "option", inner = inner } @@ -169,6 +190,13 @@ parse_type = function(stream) return parse_array_tail(stream) elseif char == "{" then return parse_struct_tail(stream) + elseif char == "$" then + local ident, err = expect_ident(stream) if err then return nil, err end + local var = stream.dict and stream.dict[ident] + if not var then + return nil, "no such schema: " .. ident + end + return var end end @@ -180,8 +208,8 @@ local function parse(stream) return type end -local function parse_throw(name, input) - local stream = { data = input, pos = 1 } +local function parse_throw(name, input, dict) + local stream = { data = input, pos = 1, dict = dict } local type, err = parse(stream) if err then error("error while parsing " .. name .. " schema at " .. stream.pos .. ": " .. err) @@ -189,6 +217,16 @@ local function parse_throw(name, input) return { kind = "named", name = name, inner = type } end +local function parse_dict(...) + local dict = {} + local args = { ... } + for i = 1, #args, 2 do + local type, err = parse_throw(args[i], args[i+1], dict) if err then return nil, err end + dict[type.name] = type.inner + end + return dict +end + local function fail(path, expect, got_val, got_type) return nil, { path = path, expect = expect, got = { val = got_val, type = got_type } } end @@ -219,6 +257,14 @@ local function deserialize(schema, data) return fail("", "base64", data) end return dec + elseif schema.kind == "uuidv4" then + if type(data) ~= "string" then + return fail("", "string", data, type(data)) + end + if not util.validate_uuidv4(data) then + return fail("", "uuidv4", data) + end + return data elseif schema.kind == "integer" then if type(data) ~= "number" then return fail("", "number", data, type(data)) @@ -233,7 +279,7 @@ local function deserialize(schema, data) end return data elseif schema.kind == "option" then - if schema ~= nil then + if data ~= nil then return wrap("?", deserialize(schema.inner, data)) end return nil @@ -277,12 +323,22 @@ local function deserialize(schema, data) map[key] = val end return map + elseif schema.kind == "set" then + local inner, err = deserialize({ kind = "array", inner = schema.inner }) + if err then + return nil, err + end + local set = {} + for _, v in ipairs(inner) do + set[v] = true + end + return set elseif schema.kind == "enum" then - if type(data.type) ~= "string" then - return fail(".type", "string", data.type, type(data.type)) + if type(data) ~= "string" then + return fail(".type", "string", data, type(data)) end - if not schema.variants[data.type] then - return fail(".type", schema.expect_variant, data.type) + if not schema.variants[data] then + return fail(".type", schema.expect_variant, data) end return data elseif schema.kind == "union" then @@ -324,5 +380,6 @@ end return { parse = parse_throw, + parse_dict = parse_dict, deserialize = deserialize_string, } @@ -27,7 +27,11 @@ local function get_local_ip() end local function migrate_save(save) - save.players = save.players or {} + local version = save.version or 0 + if version < 1 then + save.players = nil + end + save.version = 1 end local function save_data(srv) @@ -35,74 +39,162 @@ local function save_data(srv) 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 +local function broadcast(srv, pkt) + local data = util.json_enc(pkt) + for _, player in pairs(srv.players_by_peer) do + player.peer:send(data) end end -local function get_players(srv) +local function broadcast_info(srv) local players = {} - for _, player in ipairs(srv.data.players) do + for _, player in ipairs(srv.players) do table.insert(players, { - name = player.name, - active = srv.players[player.name] ~= nil, + name = player.data.name, + uuid = player.data.uuid, + faction = player.data.faction, + team = player.data.team, + ready = player.ready, + active = player.peer ~= nil, }) end - return players -end -local function get_info_pkt(srv) - return util.json_enc({ + broadcast(srv, { type = "client_info", - players = get_players(srv), + players = players, + lobby = not srv.started, + started = srv.data.players ~= nil, }) end -local function broadcast_info(srv) - local pkt = get_info_pkt(srv) - for _, clt in pairs(srv.clients) do - clt.peer:send(pkt) +local function broadcast_countdown(srv, time) + return broadcast(srv, { + type = "client_countdown", + time = time, + }) +end + +local function start_game(srv) + log("game is starting") + + if not srv.data.players then + srv.data.players = {} + for _, player in ipairs(srv.players) do + table.insert(srv.data.players, player.data) + end + save_data(srv) end + + srv.started = true + broadcast_info(srv) end -local function create_player(srv, name) - local player = { name = name } - table.insert(srv.data.players, player) - log("[server] created player " .. name) - save_data(srv) - return player +local function check_start_game(srv) + if #srv.players < 2 then + return false + end + local team = {} + local faction = {} + for _, player in ipairs(srv.players) do + if not (player.peer and player.ready and player.data.team and player.data.faction) then + return false + end + if faction[player.data.faction] then + return false + end + if team[player.data.team] and player.data.team ~= "third_party" then + return false + end + faction[player.data.faction] = true + team[player.data.team] = true + end + return team.attack and team.defend end -local function disconnect(srv, clt) - srv.clients[clt.peer] = nil - if clt.player then - srv.players[clt.player.name] = nil +local function check_auth(srv, pkt) + if pkt.version ~= common.proto_version then + return nil, "incompatible_version" end - broadcast_info(srv) + if pkt.secret ~= srv.secret then + return nil, "invalid_secret" + end + if srv.started then + return nil, "game_started" + end + if #pkt.player.name > 128 then + return nil, "name_too_long" + end + for _, p in pairs(srv.players) do + if p.peer and p.data.name == pkt.player.name then + return nil, "name_taken" + end + end + local player = srv.players_by_uuid[pkt.player.uuid] + if player then + if player.peer then + return nil, "already_active" + end + return player + end + if srv.data.players then + return nil, "no_such_player" + end +end + +local function kick(srv, peer, reason) + log_peer(srv, peer, "kicking: " .. reason) + util.send(peer, { type = "client_kick", reason = reason }) + peer:disconnect_later() end -local function select_player(srv, clt, pkt) - if #pkt.name > 128 then - return "name_too_long" +local function authenticate(srv, peer, pkt) + local player, err = check_auth(srv, pkt) + if err then + kick(srv, peer, err) + return end - if srv.players[pkt.name] then - return nil, "already_active" + + log_peer(srv, peer, "auth success, name: " .. util.display(pkt.player.name) .. ", uuid: " .. pkt.player.uuid) + + if not player then + player = { data = { uuid = pkt.player.uuid }, ready = false } + srv.players_by_uuid[player.data.uuid] = player + table.insert(srv.players, player) end - local player = get_player(srv, pkt.name) - if pkt.create and player then - return nil, "already_exists" + + player.data.name = pkt.player.name + + player.peer = peer + srv.players_by_peer[peer] = player + + player.peer:send(srv.content_pkt) + broadcast_info(srv) +end + +local function disconnect(srv, player) + if not srv.data.players then + srv.players_by_uuid[player.data.uuid] = nil + for i, p in ipairs(srv.players) do + if p == player then + table.remove(srv.players, i) + break + end + end end - if not pkt.create and not player then - return nil, "not_exists" + + if srv.started then + log("game interrupted") end + srv.started = false - if pkt.create then - player = create_player(srv, pkt.name) + srv.players_by_peer[player.peer] = nil + player.peer = nil + + for _, p in ipairs(srv.players) do + p.ready = false end - return player + + broadcast_info(srv) end function server.create(filename, match_addr) @@ -110,8 +202,21 @@ function server.create(filename, match_addr) srv.host = enet.host_create() srv.secret = util.rand_string(common.secret_len) - srv.clients = {} srv.players = {} + srv.players_by_peer = {} + srv.players_by_uuid = {} + + srv.content = { + factions = { + { id = "r6p:rainbow", name = "RAINBOW", }, + { id = "r6p:omicron", name = "OMICRON", }, + { id = "r6p:pira", name = "PIRA", }, + }, + } + srv.factions_by_id = util.index_by(srv.content.factions, "id") + + srv.content.type = "client_content" + srv.content_pkt = util.json_enc(srv.content) srv.match = srv.host:connect(match_addr or common.default_match_addr) srv.match_req = socket.gettime() @@ -124,6 +229,14 @@ function server.create(filename, match_addr) srv.data = save migrate_save(save) + if srv.data.players then + for _, data in ipairs(srv.data.players) do + local player = { data = data, ready = false } + table.insert(srv.players, player) + srv.players_by_uuid[data.uuid] = player + end + end + return srv end @@ -155,71 +268,84 @@ local function handle_match(srv, peer, pkt) end end -local schema_unauth = schema.parse("server_unauth_pkt", "union { server_hi: { secret: base64 } }") -local function handle_unauth(srv, peer, pkt) +local schema_auth = schema.parse("server_auth_pkt", [[ + union { + server_hi: { + secret: base64, + version: uint, + player: { + name: string, + uuid: uuidv4, + } + } + } +]]) +local function handle_auth(srv, peer, pkt) if pkt.type == "server_hi" then - if pkt.secret == srv.secret then - log_peer(srv, peer, "auth success") - local clt = { peer = peer } - srv.clients[peer] = clt - util.send(peer, { - type = "client_hi", - }) - peer:send(get_info_pkt(srv)) - else - log_peer(srv, peer, "auth failure") - util.send(peer, { type = "client_reject" }) - peer:disconnect_later() - end + authenticate(srv, peer, pkt) end end -local schema_unnamed = schema.parse("server_unnamed_pkt", "union { server_player: { name: string, create: boolean } }") -local function handle_unnamed(srv, peer, pkt) - local clt = srv.clients[peer] - if pkt.type == "server_player" then - local player, err = select_player(srv, clt, pkt) - if err then - log_peer(srv, peer, "failed to select player") - util.send(clt.peer, { - type = "client_player_fail", - error = err, - }) +local schema_player = schema.parse("server_pkt", [[union { + server_select: { + item: union { + ready: { ready: boolean }, + faction: { faction: string }, + team: { team: $team }, + } + }, +}]], common.dict) +local function handle_player(srv, peer, pkt) + local player = srv.players_by_peer[peer] + if pkt.type == "server_select" then + local item = pkt.item.type + local value = pkt.item[item] + + if item == "ready" then + player[item] = value else - log_peer(srv, peer, "selected player " .. player.name) - srv.players[player.name] = clt - clt.player = player - util.send(clt.peer, { - type = "client_player", - name = player.name, - }) - broadcast_info(srv) + if srv.data.players then + -- illegal + return + end + + player.data[item] = value end + broadcast_info(srv) end end -local schema_player = schema.parse("server_pkt", [[ - union { - } -]]) -local function handle_player(srv, peer, pkt) -end - peer_info = function(srv, peer) if peer == srv.match then return { name = "match server", schema = schema_match, handle = handle_match } end - local clt = srv.clients[peer] - if not clt then - return { name = "unauthentiated(" .. tostring(peer) .. ")", schema = schema_unauth, handle = handle_unauth } - elseif not clt.player then - return { name = "unnamed(" .. tostring(peer) .. ")", schema = schema_unnamed, handle = handle_unnamed } + local player = srv.players_by_peer[peer] + if not player then + return { name = "unauthentiated(" .. tostring(peer) .. ")", schema = schema_auth, handle = handle_auth } else - return { name = "player(" .. util.display(clt.player.name) .. ")", schema = schema_player, handle = handle_player } + return { name = "player(" .. util.display(player.data.name) .. ")", schema = schema_player, handle = handle_player } end end function server.update(srv, wait) + if not srv.started then + local startable = check_start_game(srv) + local countdown = 3 + + if startable then + if not srv.start_countdown then + srv.start_countdown = socket.gettime() + broadcast_countdown(srv, countdown) + elseif srv.start_countdown + countdown <= socket.gettime() then + srv.start_countdown = nil + start_game(srv) + end + elseif not startable and srv.start_countdown then + srv.start_countdown = nil + broadcast_countdown(srv, nil) + end + end + local event = srv.host:service(wait) while event do if event.type == "receive" then @@ -241,9 +367,9 @@ function server.update(srv, wait) if event.peer == srv.match then -- TODO else - local clt = srv.clients[event.peer] - if clt then - disconnect(srv, clt) + local player = srv.players_by_peer[event.peer] + if player then + disconnect(srv, player) end end end @@ -263,12 +389,7 @@ 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() + util.close_host(srv.host) end return server diff --git a/test_client.lua b/test_client.lua deleted file mode 100755 index aac6fe6..0000000 --- a/test_client.lua +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env lua5.1 -local client = require("client") - -local invite = assert(arg[1]) -local clt = client.join(invite) -local started - -while true do - client.update(clt) - local status = client.status(clt) - - if status == "timeout_match" then - print("failed to connect to match server") - break - elseif status == "fail_match" then - print("game not found (invalid invite?)") - break - elseif status == "timeout_server" then - print("failed to connect to server") - break - elseif status == "fail_server" then - print("incorrect secret (invalid invite?)") - break - elseif status == "disco" then - print("lost connection to server") - break - elseif status == "active" and not started then - started = true - print("connected to " .. tostring(clt.server)) - end -end - -client.close(clt) @@ -1,13 +1,10 @@ -local ok, bit = pcall(require, "bit") -if not ok then - ok, bit = pcall(require, "bit32") - if not ok then error("no bit library") end -end - -local utf8 = require("utf8") +local bit = require("util").bit +local utf8 = require("util").utf8 local distribute = require("distribute") -local function ui_new(root) +local ui = {} + +function ui.new(root) return { targets = {}, root = root } end @@ -18,9 +15,56 @@ local function compute(x, ...) return x end -local function access(t, k) - if t ~= nil then - return t[k] +local function access(x, ...) + for _, f in ipairs({ ... }) do + if x == nil then + return nil + end + x = x[f] + end + return x +end + +local function filter(cb, f, s, i) + local function get_next(s, i) + local i, x = f(s, i) + if i == nil or x == nil then + return + end + if not cb(i, x) then + return get_next(s, i) + end + return i, x + end + return get_next, s, i +end + +local function ripairs(t) + local function get_next(t, i) + if i > 0 then + return i-1, t[i] + end + end + return get_next, t, #t +end + +local function active_children(node, iter) + return filter(function(_, child) return child.active ~= false end, (iter or ipairs)(node)) +end + +local function flip(dir) + if dir == "x" then + return "y" + elseif dir == "y" then + return "x" + end +end + +local function dir_sin(dir) + if dir == "x" then + return math.cos + elseif dir == "y" then + return math.sin end end @@ -42,17 +86,45 @@ local function add_computed(node, attr, k, v) node.computed[attr][k] = v end -local function node_compute_min_size(node, dir) +local function node_shift(node, dir, side, offs) + local angle = access(node.angle, dir, side) + if not angle then + return 0 + end + return dir_sin(dir)(angle) * offs +end + +local function child_shift(node, child, dir) + local pos = (access(child.computed.pos, flip(dir)) or 0) - (access(node.computed.pos, flip(dir)) or 0) + local size = access(child.computed.size, flip(dir)) or 0 + + local off = {} + for i, f in ipairs({ math.max, math.min }) do + off[i] = f(node_shift(node, dir, i, pos), node_shift(node, dir, i, pos+size)) + end + + return off[1], off[2] +end + +function ui.min_size(node, dir) + for _, child in active_children(node) do + ui.min_size(child, dir) + end + 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) + for _, child in active_children(node) do + local child_size = child.computed.min_size[dir] if node.stack == dir then size = size + child_size else + if node.angle then + local off_pos, off_size = child_shift(node, child, dir) + child_size = child_size + off_pos - off_size + end size = math.max(size, child_size) end end @@ -70,8 +142,17 @@ local function node_compute_layout(node, dir, pos, size) 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)) + for _, child in active_children(node) do + local child_size = size + local child_pos = pos + + if node.angle then + local off_pos, off_size = child_shift(node, child, dir) + child_pos = child_pos + off_pos + child_size = child_size - off_pos + off_size + end + + node_compute_layout(child, dir, align(align_as, child_pos, child.computed.min_size[dir], child_size)) end return end @@ -79,7 +160,7 @@ local function node_compute_layout(node, dir, pos, size) local min_size_noflex = 0 local flex_elems = {} local has_flex - for i, child in ipairs(node) do + for i, child in active_children(node) do if child.flex then has_flex = true flex_elems[i] = { @@ -97,7 +178,7 @@ local function node_compute_layout(node, dir, pos, size) pos = align(align_as, pos, min_size_noflex, size) end - for i, child in ipairs(node) do + for i, child in active_children(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 @@ -105,10 +186,10 @@ local function node_compute_layout(node, dir, pos, size) 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) + ui.min_size(tree, "y") + ui.min_size(tree, "x") node_compute_layout(tree, "y", y, height) + node_compute_layout(tree, "x", x, width) end -- render @@ -121,37 +202,146 @@ local function update_flags(node, targets, flags) return new end +function ui.modif_attr(attr) + return function(self, f) + return (f.press and self[attr.."_press"]) or + (f.focus and self[attr.."_focus"]) or + (f.hover and self[attr.."_hover"]) or + self[attr.."_normal"] + end +end + +function ui.attr(node, attr, flags) + return compute(node[attr] or ui.modif_attr(attr), node, flags) +end + +local stencil_id, reset_stencil +do + local id + stencil_id = function() id = id + 1 return id end + reset_stencil = function() id = 0 end +end + +local function rect_radius(angle, size) + local x = size.x * math.min(0.5, math.abs(math.cos(angle)*math.sqrt(1/2))) + local y = size.y * math.min(0.5, math.abs(math.sin(angle)*math.sqrt(1/2))) + return math.sqrt(x^2+y^2) +end + +local function vec_len(x, y) + return math.sqrt(x^2+y^2) +end + local function node_render(node, targets, flags) + compute(node.update, node, flags) + + if node.visible ~= nil and not compute(node.visible, node, flags) then + return + end + 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) + local outline = { + pos.x, pos.y, + pos.x + size.x, pos.y + node_shift(node, "y", 1, size.x), + pos.x + size.x + node_shift(node, "x", 2, size.y), pos.y + size.y + node_shift(node, "y", 2, size.x), + pos.x + node_shift(node, "x", 1, size.y), pos.y + size.y, + } + + local fill = ui.attr(node, "fill", flags) + if fill then + love.graphics.setColor(fill) + love.graphics.polygon("fill", outline) 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) + if node.gradient then + local id = stencil_id() + love.graphics.setColor(1, 1, 1) + love.graphics.stencil(function() + love.graphics.polygon("fill", outline) + end, "replace", id) + love.graphics.setStencilTest("equal", id) + + local center = { x = size.x/2, y = size.y/2 } + local diag = vec_len(size.x/2, size.y/2) + + if node.gradient_offset then + local offset = rect_radius(node.gradient_angle, size)*node.gradient_offset + local off_x = math.cos(node.gradient_angle)*offset + local off_y = math.sin(node.gradient_angle)*offset + diag = vec_len(center.x+math.abs(off_x), center.y+math.abs(off_y)) + center.x = center.x + off_x + center.y = center.y + off_y + end + + love.graphics.draw(node.gradient, pos.x+center.x, pos.y+center.y, node.gradient_angle, diag*2, diag*2) + love.graphics.setStencilTest() + end + local line = ui.attr(node, "line", flags) + if line then + love.graphics.setColor(line) + love.graphics.setLineWidth(compute(node.line_width, node, flags) or 1) + local segments = compute(node.line_segments, node, flags) + if segments then + for _, s in ipairs(segments) do + local a = (s-1)*2+1 + local b = ((s-1+1)%4)*2+1 + love.graphics.line( outline[a], outline[a+1], outline[b], outline[b+1]) + end + else + love.graphics.polygon("line", outline) + end + end + if node.image then + love.graphics.setColor(ui.attr(node, "color", flags) or { 1, 1, 1 }) + + local w, h = node.image:getDimensions() + local scale_x, scale_y + local pos_x, pos_y = pos.x, pos.y + + if node.image_mode == "scale" then + scale_x = size.x/w + scale_y = size.y/h + elseif node.image_mode == "cover" then + scale_x = math.max(size.x/w, size.y/h) + scale_y = scale_x + pos_x = align("center", pos_x, w*scale_x, size.x) + pos_y = align("center", pos_y, h*scale_y, size.y) + else + error(node.image_mode) + end + + love.graphics.draw(node.image, pos_x, pos_y, 0, scale_x, scale_y) end if node.text then - love.graphics.setColor(compute(node.color, node, flags)) + love.graphics.setColor(ui.attr(node, "color", flags) or { 1, 1, 1 }) love.graphics.draw(node.text, pos.x, pos.y) end - for _, child in ipairs(node) do + for _, child in active_children(node) do node_render(child, targets, flags) end end -local function ui_render(ui) - node_render(ui.root, ui.targets, {}) +function ui.render(u) + reset_stencil() + node_render(u.root, u.targets, {}) end -- events +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 node_get_target(node, event, x, y) - -- todo: maybe reverse iteration? - for _, child in ipairs(node) do + for _, child in active_children(node, ripairs) do local hov = node_get_target(child, event, x, y) if hov then return hov @@ -168,42 +358,32 @@ local function node_get_target(node, event, x, y) end end -local function ui_get_target(ui, event) - return node_get_target(ui.root, event, love.mouse.getPosition()) +local function get_target(u, event) + return node_get_target(u.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] +local function set_target(u, event, elem) + local old = u.targets[event] if old ~= elem then fire_event(old, event.."_end") fire_event(elem, event) end - ui.targets[event] = elem + u.targets[event] = elem end -local function ui_update_target(ui, event) - ui_set_target(ui, event, ui_get_target(ui, event)) +function update_target(u, event) + set_target(u, event, get_target(u, event)) end -local function ui_mousepressed(ui, x, y, button) +function ui.mousepressed(u, x, y, button) if button ~= 1 then return false end - ui_update_target(ui, "press") - ui_update_target(ui, "focus") + update_target(u, "press") + update_target(u, "focus") - local focus = ui.targets.focus + local focus = u.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) @@ -211,23 +391,23 @@ local function ui_mousepressed(ui, x, y, button) love.keyboard.setTextInput(false) end - if ui.targets.press or ui.targets.focus then + if u.targets.press or u.targets.focus then return true end return false end -local function ui_mousereleased(ui, x, y, button) +function ui.mousereleased(u, 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 + local press = get_target(u, "press", love.mouse.getPosition()) + if press == u.targets.press then fire_event(press, "click") end - ui_set_target(ui, "press", nil) + set_target(u, "press", nil) if press then return true @@ -235,15 +415,15 @@ local function ui_mousereleased(ui, x, y, button) return false end -local function ui_textinput(ui, text) - return fire_event(ui.targets.focus, "input", text) +function ui.textinput(u, text) + return fire_event(u.targets.focus, "input", text) end -local function ui_keypressed(ui, key) +function ui.keypressed(u, key) if key == "backspace" then - return fire_event(ui.targets.focus, "input", "\b") + return fire_event(u.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()) + return fire_event(u.targets.focus, "input", love.system.getClipboardText()) end return false @@ -251,22 +431,23 @@ end -- update -local function ui_update(ui) - tree_compute_layout(ui.root, 0, 0, love.graphics:getDimensions()) - ui_update_target(ui, "hover") +function ui.update(u) + tree_compute_layout(u.root, 0, 0, love.graphics:getDimensions()) + update_target(u, "hover") end -- presets -local function color(hex) +function ui.color(hex) return { love.math.colorFromBytes( bit.band(bit.rshift(hex, 16), 0xff), bit.band(bit.rshift(hex, 8), 0xff), bit.band(bit.rshift(hex, 0), 0xff) ) } end +local C = ui.color -local function ui_flex(n, ...) +function ui.flex(n, ...) return { flex = n, ... } end @@ -283,95 +464,141 @@ local function pad(dir, size, ...) end return x elseif #nodes == 1 then - return { stack = dir, p, ui_flex(1, nodes[1]), p } + return { stack = dir, p, ui.flex(1, nodes[1]), p } end return p end -local function ui_pad_x(...) +function ui.pad_x(...) return pad("x", ...) end -local function ui_pad_y(...) +function ui.pad_y(...) return pad("y", ...) end -local function center_x(...) +function ui.center_x(...) return { align = { x = "center" }, ... } end -local function center_y(...) +function ui.center_y(...) return { align = { y = "center" }, ... } end -local function stack_x(...) +function ui.align_left(...) + return { align = { x = "start" }, ... } +end + +function ui.align_right(...) + return { align = { x = "end" }, ... } +end + +function ui.align_top(...) + return { align = { y = "start" }, ... } +end + +function ui.align_bottom(...) + return { align = { y = "end" }, ... } +end + +function ui.stack_x(...) return { stack = "x", ... } end -local function stack_y(...) +function ui.stack_y(...) return { stack = "y", ... } end -local function ui_text(str, size, col) +local font_cache = {} +local function get_font(size) + if not font_cache[size] then + font_cache[size] = love.graphics.newFont("assets/font/DINEngschrift Regular.ttf", size) + end + return font_cache[size] +end + +function ui.text(str, size, col) return { color = col, - text = love.graphics.newText(love.graphics.newFont(size), str), + text = love.graphics.newText(get_font(size), str), size = function(self) return { x = self.text:getWidth(), y = self.text:getHeight() } end, } end -local function ui_button_elem(elem, click) +function ui.image(path, mode, size) + local image = love.graphics.newImage(path) + local w, h = image:getDimensions() + + size = size or {} + if mode == "scale" then + if not size.x then + size.x = w * (size.y or h) / h + end + if not size.y then + size.y = h * size.x / w + end + end + 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 self.fill_hover or self.fill_normal - end, - line = function(self, f) - return f.hover and self.line_hover or self.line_normal - end, - events = { hover = true, press = true, click = click, }, - ui_pad_y(5, ui_pad_x(10, elem)) + image = image, + image_mode = mode, + size = size, } end -local function ui_button(label, click) - return ui_button_elem(center_x(ui_text(label, 25, color(0x000000))), click) +function ui.click(elem, click) + elem.events = { hover = true, press = true, click = click } + return elem +end + +function ui.button_elem(elem, click) + return ui.click({ + fill_normal = C(0x808080), + fill_press = C(0x606080), + fill_hover = C(0x8080a0), + line = C(0x505050), + ui.pad_y(5, ui.pad_x(5, elem)), + }, click) +end + +function ui.button(label, click) + local label = ui.text(label, 25, C(0x000000)) + local btn = ui.button_elem(ui.center_x(ui.center_y(label)), click) + btn.label = label + return btn end -local function ui_input(placeholder) +function ui.input(placeholder) local function is_placeholder(self, focus) - return self.input_value == "" and not focus + return self.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() + self.text_elem.text:set(is_placeholder(self, focus) and self.placeholder or self.value) + self.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) + local text = ui.text(placeholder, 25, function(self, f) + return is_placeholder(self.input_parent, f.focus) and C(0xb0b0b0) or C(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) + fill = C(0x000000), + visible = function(self, f) + return f.focus and + math.floor(love.timer.getTime() - self.input_parent.interact_time) % 2 == 0 end } -- TODO: cursor + -- TODO: selection -- 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, + fill = C(0xffffff), + line_focus = C(0x5050ff), + line_normal = C(0x505050), events = { focus = function(self) update_text(self, true) @@ -381,46 +608,87 @@ local function ui_input(placeholder) end, input = function(self, text) if text == "\b" then - local byteoffset = utf8.offset(self.input_value, -1) + local byteoffset = utf8.offset(self.value, -1) if byteoffset then - self.input_value = string.sub(self.input_value, 1, byteoffset - 1) + self.value = string.sub(self.value, 1, byteoffset - 1) end else - self.input_value = self.input_value .. text + self.value = self.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))) + value = "", + placeholder = placeholder, + text_elem = text, + ui.pad_y(5, ui.pad_x(10, ui.stack_x(text, marker))) } + function elem:set_value(val) + self.value = val + update_text(self, false) + end + 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, - button_elem = ui_button_elem, - input = ui_input, -} +function ui.box(title, elem) + local title = ui.text(title, 30, C(0x000000)) + local inner = { ui.pad_y(15, ui.center_x(title), elem) } + return { + title = title, + inner = inner, + fill = C(0xa0a0a0), + ui.pad_x(20, ui.stack_y(ui.pad_y(15), inner, ui.pad_y(20))) + } +end + +function ui.dialog_box(title, elem, width) + local box = ui.box(title, elem) + local u = ui.new(ui.center_y(ui.stack_x( + ui.flex(1), + ui.flex(width or 2, box), + ui.flex(1) + ))) + u.title = box.title + u.inner = box.inner + return u +end + +function ui.popup_box(title, action_name, action, show) + local box = ui.box(title, ui.button(action_name, action)) + local u = ui.new(ui.center_x(ui.center_y(box))) + u.title = box.title + u.inner = box.inner + u.show = show + return u +end + +function ui.copy_button(contents, size) + size = size or 25 + return ui.button_elem(ui.image("assets/icon/copy.png", "scale", { x = size, y = size }), function() + love.system.setClipboardText(compute(contents)) + end) +end + +function ui.gradient(angle, offset, colors, elem) + local mesh = {} + local function point(col, x, y) + table.insert(mesh, { x, y, x, y, col[1], col[2], col[3], col[4] or 1 }) + end + for i, col in ipairs(colors) do + point(col, (i-1)/(#colors-1)-0.5, 0-0.5) + point(col, (i-1)/(#colors-1)-0.5, 1-0.5) + end + return { + gradient = love.graphics.newMesh(mesh, "strip", "static"), + gradient_offset = offset, + gradient_angle = angle/180*math.pi, + elem + } +end + +return ui @@ -1,11 +1,19 @@ local base64 = require("vendor.base64") local json = require("vendor.JSON") local table_unpack = table.unpack or unpack -local has_utf8, utf8 = pcall(require, "utf8") -if not has_utf8 then - utf8 = require("lua-utf8") + +local function fallback_require(name, fallback) + local ok, lib = pcall(require, name) + if not ok then + ok, lib = pcall(require, fallback) + if not ok then error("no "..name.." library") end + end + return lib end +local utf8 = fallback_require("utf8", "lua-utf8") +local bit = fallback_require("bit", "bit32") + local function base64_dec(x) local succ, dec = pcall(base64.decode, x) if succ then return dec end @@ -24,9 +32,19 @@ local function send(peer, x) peer:send(json_enc(x)) end -local rand_string +local function close_host(host) + local peers = host:peer_count() + for i = 1, peers do + host:get_peer(i):disconnect() + end + host:flush() + host:destroy() +end + +local random, rand_string if love then + random = love.math.random rand_string = function(n) local b = {} for i = 1, n do @@ -34,17 +52,43 @@ if love then end return string.char(table_unpack(b)) end - mkdir = love.filesystem.mkdir else - local rand_file + local function decode_le(str) + local x = 0 + for i = 1, #str do + x = bit.bor(x, bit.lshift(str:byte(i,i), (i-1)*8)) + end + return x + end + + local rand_file = io.open("/dev/random", "r") + math.randomseed(decode_le(rand_file:read(4))) + + random = math.random rand_string = function(n) - local rand_file = rand_file or io.open("/dev/random") return rand_file:read(n) end end +local uuidv4_format = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" +local uuidv4_pattern = "^"..uuidv4_format + :gsub("%-", "%%%-") + :gsub("[xy]", function(c) + return (c == "x") and "[0-9a-f]" or "[8-9a-b]" + end).."$" + +local function gen_uuidv4() + return uuidv4_format:gsub("[xy]", function(c) + return ("%x"):format((c == "x") and random(0, 0xf) or random(8, 0xb)) + end) +end + +local function validate_uuidv4(x) + return x:match(uuidv4_pattern) ~= nil +end + local function split_addr(addr) - local host, port = addr:match("(.*):([^:]*)$") + local host, port = addr:match("^(.*):([^:]*)$") return { host = host, port = port } end @@ -90,15 +134,47 @@ local function log(cat, msg) print("["..cat.."] " .. msg) end +local function index_by(t, k) + local indexed = {} + for _, v in ipairs(t) do + indexed[v[k]] = v + end + return indexed +end + +local function find_by(t, k, v) + for _, x in ipairs(t) do + if x[k] == v then + return x + end + end +end + +local function find_index_by(t, k, v) + for i, x in ipairs(t) do + if x[k] == v then + return i + end + end +end + return { - rand_string = rand_string, - mkdir = mkdir, + utf8 = utf8, + bit = bit, base64_dec = base64_dec, base64_enc = base64.encode, json_dec = json_dec, json_enc = json_enc, send = send, + close_host = close_host, + random = random, + rand_string = rand_string, + gen_uuidv4 = gen_uuidv4, + validate_uuidv4 = validate_uuidv4, split_addr = split_addr, display = display, log = log, + index_by = index_by, + find_by = find_by, + find_index_by = find_index_by, } |
