summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLizzy Fleckenstein <lizzy@vlhl.dev>2026-06-21 03:12:51 +0200
committerLizzy Fleckenstein <lizzy@vlhl.dev>2026-06-21 03:12:51 +0200
commit2ce0500f739cf68b7a5b475c6050ecdbf107fa93 (patch)
treeed10a48d8289f56c4d0c4b35551a11b50c6ef327
parent0fa7e4e3c6b8989314759cb4c99c1bab262a431d (diff)
downloadr6p-2ce0500f739cf68b7a5b475c6050ecdbf107fa93.tar.xz
update lobby
-rw-r--r--ATTRIBUTION6
-rw-r--r--assets/bg/rainbow_six_siege_x_clash.jpgbin0 -> 748017 bytes
-rw-r--r--assets/font/DINEngschrift Regular.ttfbin0 -> 40908 bytes
-rw-r--r--assets/icon/angle_left.pngbin0 -> 4100 bytes
-rw-r--r--assets/icon/angle_right.pngbin0 -> 3989 bytes
-rw-r--r--assets/icon/copy.pngbin0 -> 605 bytes
-rw-r--r--client.lua159
-rw-r--r--common.lua6
-rw-r--r--conf.lua3
-rw-r--r--dialog.lua50
-rw-r--r--lang.lua27
-rw-r--r--lang/en.lua88
-rw-r--r--lobby.lua272
-rw-r--r--main.lua367
-rw-r--r--main_menu.lua279
-rw-r--r--save_file.lua4
-rw-r--r--schema.lua77
-rw-r--r--server.lua321
-rwxr-xr-xtest_client.lua33
-rw-r--r--ui.lua538
-rw-r--r--util.lua96
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
new file mode 100644
index 0000000..3ef22ea
--- /dev/null
+++ b/assets/bg/rainbow_six_siege_x_clash.jpg
Binary files differ
diff --git a/assets/font/DINEngschrift Regular.ttf b/assets/font/DINEngschrift Regular.ttf
new file mode 100644
index 0000000..5e0476b
--- /dev/null
+++ b/assets/font/DINEngschrift Regular.ttf
Binary files differ
diff --git a/assets/icon/angle_left.png b/assets/icon/angle_left.png
new file mode 100644
index 0000000..7821eb0
--- /dev/null
+++ b/assets/icon/angle_left.png
Binary files differ
diff --git a/assets/icon/angle_right.png b/assets/icon/angle_right.png
new file mode 100644
index 0000000..4995daa
--- /dev/null
+++ b/assets/icon/angle_right.png
Binary files differ
diff --git a/assets/icon/copy.png b/assets/icon/copy.png
new file mode 100644
index 0000000..dc94acc
--- /dev/null
+++ b/assets/icon/copy.png
Binary files differ
diff --git a/client.lua b/client.lua
index 7246324..81c72d6 100644
--- a/client.lua
+++ b/client.lua
@@ -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
diff --git a/common.lua b/common.lua
index 98268ab..8b02d21 100644
--- a/common.lua
+++ b/common.lua
@@ -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,
+}
diff --git a/main.lua b/main.lua
index a5dd579..277def2 100644
--- a/main.lua
+++ b/main.lua
@@ -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
diff --git a/schema.lua b/schema.lua
index 2690d35..4cb615a 100644
--- a/schema.lua
+++ b/schema.lua
@@ -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,
}
diff --git a/server.lua b/server.lua
index 5853c3a..94a2265 100644
--- a/server.lua
+++ b/server.lua
@@ -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)
diff --git a/ui.lua b/ui.lua
index 60f0833..f466165 100644
--- a/ui.lua
+++ b/ui.lua
@@ -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
diff --git a/util.lua b/util.lua
index df7a635..7543af3 100644
--- a/util.lua
+++ b/util.lua
@@ -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,
}