Files
factorio-clusterio/plugins/lobby/module/lobby.lua
T
Danielv123 a5bd43637e Enable the player proxy by default and give lobbies an empty save
The player proxy now listens on 34197 by default. New hosts default to
the port range 34200-34299 so they don't hand that port to instances,
and the controller logs an error and runs without the proxy if the port
is taken, as hosts with the old range only skip it when the controller
already holds it.

Lobby instances start the save lobby.zip, created on first start as a
one tile map with no autoplace and with the freeplay scenario replaced by
an empty one. This uses the new chooseSave instance hook, which lets
plugins pick or create the save to start, and setSaveScenario from
@clusterio/host, which sets the scenario the save patcher keeps loading.
Map gen settings can't use a width and height of 0 for this, as Factorio
takes 0 to mean an unlimited map.

The address players reach the proxy on moves from lobby.public_address
to controller.player_proxy_public_address. When it's set the player_auth
login page lists it once for the instances behind the proxy instead of
their own addresses, and lobbies with player_auth loaded get a button
that opens the web interface login dialog.
2026-10-29 20:54:11 +01:00

270 lines
8.0 KiB
Lua

local clusterio_api = require("modules/clusterio/api")
local compat = require("modules/clusterio/compat")
local mod_gui = require("mod-gui")
--- @class LobbyServer
--- @field id integer
--- @field name string
--- @field status string
--- @field version string?
--- @field players integer
--- @class LobbyScriptData
--- @field is_lobby boolean
--- @field instance_id integer?
--- @field web_login boolean Show button for logging into the web interface with the player_auth plugin
--- @field servers LobbyServer[]
--- @field status table<string, string> Status text by player name
local status_captions = {
running = "Online",
stopped = "Offline",
starting = "Starting",
stopping = "Stopping",
creating_save = "Creating save",
exporting_data = "Exporting data",
}
--- Idempotent accessor for the persistent state of the module
--- @return LobbyScriptData
local function get_script_data()
local data = compat.script_data.lobby
if not data then
data = {}
compat.script_data.lobby = data
end
data.servers = data.servers or {}
data.status = data.status or {}
if data.is_lobby == nil then
data.is_lobby = false
end
return data
end
--- Reason a server can't be joined from here, or nil if it can
--- @param data LobbyScriptData
--- @param server LobbyServer
--- @return string?
local function unjoinable_reason(data, server)
if server.id == data.instance_id then
return "You are on this server"
end
local version = compat.active_mods["base"]
if server.version and server.version ~= version then
return "Runs Factorio " .. server.version .. ", you have " .. version
end
if not status_captions[server.status] then
return "Not available"
end
return nil
end
--- @param player LuaPlayer
--- @param data LobbyScriptData
local function build_content(player, data, content)
content.clear()
local status = data.status[player.name]
if status then
local label = content.add { type = "label", caption = status }
label.style.single_line = false
label.style.maximal_width = 500
label.style.bottom_margin = 8
end
if #data.servers == 0 then
content.add { type = "label", caption = "There are no servers to join right now." }
return
end
local servers = content.add { type = "table", name = "lobby_servers", column_count = 4, style = "bordered_table" }
servers.style.minimal_width = 420
for _, caption in ipairs({ "Server", "Players", "Status", "" }) do
servers.add { type = "label", caption = caption, style = "caption_label" }
end
for _, server in ipairs(data.servers) do
servers.add { type = "label", caption = server.name }
servers.add { type = "label", caption = tostring(server.players) }
servers.add { type = "label", caption = status_captions[server.status] or server.status }
local reason = unjoinable_reason(data, server)
servers.add {
type = "button",
caption = server.status == "stopped" and "Start and join" or "Join",
tooltip = reason,
enabled = reason == nil,
tags = { lobby_instance_id = server.id },
}
end
end
--- @param player LuaPlayer
local function open_browser(player)
local data = get_script_data()
local frame = player.gui.screen.lobby_frame
if not frame then
frame = player.gui.screen.add { type = "frame", name = "lobby_frame", direction = "vertical",
caption = data.is_lobby and "Pick a server to join" or "Servers" }
frame.auto_center = true
local inner = frame.add { type = "frame", name = "content", direction = "vertical",
style = "inside_shallow_frame_with_padding" }
inner.style.horizontally_stretchable = true
local buttons = frame.add { type = "flow", name = "buttons", direction = "horizontal" }
buttons.style.top_margin = 8
if data.web_login then
buttons.add { type = "button", name = "lobby_web_login", caption = "Log in to web interface",
tooltip = "Same as the /web-login command" }
end
local filler = buttons.add { type = "empty-widget" }
filler.style.horizontally_stretchable = true
if not data.is_lobby then
buttons.add { type = "button", name = "lobby_close", caption = "Close" }
end
end
build_content(player, data, frame.content)
end
--- @param player LuaPlayer
local function close_browser(player)
if player.gui.screen.lobby_frame then
player.gui.screen.lobby_frame.destroy()
end
end
--- @param player LuaPlayer
local function refresh_player(player)
local data = get_script_data()
local button_flow = mod_gui.get_button_flow(player)
if data.is_lobby then
if button_flow.lobby_toggle then
button_flow.lobby_toggle.destroy()
end
open_browser(player)
else
if not button_flow.lobby_toggle then
button_flow.add { type = "button", name = "lobby_toggle", caption = "Servers", style = mod_gui.button_style }
end
if player.gui.screen.lobby_frame then
open_browser(player)
end
end
end
local function refresh_all()
for _, player in pairs(game.connected_players) do
refresh_player(player)
end
end
--- Keep players in a lobby from interacting with its world
--- @param player LuaPlayer
local function make_spectator(player)
if player.controller_type == defines.controllers.cutscene then
player.exit_cutscene()
end
if player.character then
player.character.destroy()
end
if player.controller_type ~= defines.controllers.spectator then
player.set_controller { type = defines.controllers.spectator }
end
end
local lobby_module = {}
lobby_module.events = {}
lobby_module.events[clusterio_api.events.on_server_startup] = function()
get_script_data()
end
lobby_module.events[defines.events.on_player_joined_game] = function(event)
local player = game.get_player(event.player_index)
local data = get_script_data()
data.status[player.name] = nil
if data.is_lobby then
make_spectator(player)
end
refresh_player(player)
end
lobby_module.events[defines.events.on_gui_click] = function(event)
local element = event.element
if not element.valid then
return
end
local player = game.get_player(event.player_index)
if element.name == "lobby_toggle" then
if player.gui.screen.lobby_frame then
close_browser(player)
else
open_browser(player)
end
elseif element.name == "lobby_close" then
close_browser(player)
elseif element.name == "lobby_web_login" then
clusterio_api.send_json("player_auth", { type = "open_dialog", player = player.name })
elseif element.tags and element.tags.lobby_instance_id then
local data = get_script_data()
local instance_id = element.tags.lobby_instance_id
local name = tostring(instance_id)
for _, server in ipairs(data.servers) do
if server.id == instance_id then
name = server.name
end
end
data.status[player.name] = "Connecting to " .. name .. "..."
clusterio_api.send_json("lobby:connect", { player = player.name, instance_id = instance_id })
open_browser(player)
end
end
-- Interface used by the instance plugin through RCON
lobby = {}
--- @param is_lobby boolean
--- @param instance_id integer
--- @param web_login boolean?
function lobby.configure(is_lobby, instance_id, web_login)
local data = get_script_data()
data.is_lobby = is_lobby
data.instance_id = instance_id
data.web_login = web_login == true
-- Rebuilt so the frame matches the new configuration
for _, player in pairs(game.players) do
close_browser(player)
end
refresh_all()
end
--- @param json string
function lobby.set_servers(json)
local data = get_script_data()
data.servers = compat.json_to_table(json) or {}
refresh_all()
end
--- @param player_name string
--- @param text string
function lobby.set_player_status(player_name, text)
local data = get_script_data()
data.status[player_name] = text
local player = game.get_player(player_name)
if player and player.connected then
open_browser(player)
end
end
--- @param player_name string
--- @param address string
--- @param server_name string
function lobby.connect_player(player_name, address, server_name)
local data = get_script_data()
local player = game.get_player(player_name)
if not player or not player.connected then
return
end
data.status[player_name] = "Moving you to " .. server_name .. "..."
open_browser(player)
player.connect_to_server { address = address, name = server_name }
end
return lobby_module