Files
Danielv123 00823262d5 Create lobbies on demand and only list servers on the player's version
When a player connects with a Factorio version and set of built in mods
no lobby runs, the lobby plugin now creates one on a connected host that
has the version installed or can download it, with a mod pack matching
the player's built in mods. This lets a cluster accept players on
several Factorio versions on the same port without any setup. It can be
turned off with lobby.auto_create.

To pick the mod pack the proxy now waits for the whole connection
confirm before routing and passes the mods listed in it to the
playerProxyRoute hook. Hosts answer the new HostFactorioVersionsRequest
with the versions they have installed.

The server list in lobbies only shows instances on the Factorio version
of the lobby, with a checkbox to also show the others greyed out.
2026-10-29 20:54:11 +01:00

327 lines
9.7 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
--- @field show_other_versions table<string, boolean> Players listing servers on other Factorio versions
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 {}
data.show_other_versions = data.show_other_versions 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
--- True if the server runs a different Factorio version than this one
--- @param server LobbyServer
--- @return boolean
local function other_version(server)
return server.version ~= nil and server.version ~= compat.active_mods["base"]
end
--- @param player LuaPlayer
--- @param data LobbyScriptData
--- @param content LuaGuiElement
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
-- Servers on other versions can't be joined from here, so they are only listed if asked for
local show_other = data.show_other_versions[player.name] == true
local shown = {}
local other_count = 0
for _, server in ipairs(data.servers) do
if not other_version(server) then
shown[#shown + 1] = server
else
other_count = other_count + 1
end
end
if show_other then
for _, server in ipairs(data.servers) do
if other_version(server) then
shown[#shown + 1] = server
end
end
end
if #shown == 0 then
content.add {
type = "label",
caption = "There are no servers running Factorio " .. compat.active_mods["base"] .. " to join right now.",
}
else
local servers = content.add {
type = "table", name = "lobby_servers", column_count = 5, style = "bordered_table",
}
servers.style.minimal_width = 480
for _, caption in ipairs({ "Server", "Version", "Players", "Status", "" }) do
servers.add { type = "label", caption = caption, style = "caption_label" }
end
for _, server in ipairs(shown) do
local reason = unjoinable_reason(data, server)
local name = servers.add { type = "label", caption = server.name }
servers.add { type = "label", caption = server.version or "" }
servers.add { type = "label", caption = tostring(server.players) }
servers.add { type = "label", caption = status_captions[server.status] or server.status }
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 },
}
if other_version(server) then
name.style.font_color = { 0.5, 0.5, 0.5 }
end
end
end
if other_count > 0 then
local checkbox = content.add {
type = "checkbox",
name = "lobby_show_other_versions",
caption = "Show " .. other_count .. " servers on other Factorio versions",
state = show_other,
}
checkbox.style.top_margin = 8
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
lobby_module.events[defines.events.on_gui_checked_state_changed] = function(event)
local element = event.element
if not element.valid or element.name ~= "lobby_show_other_versions" then
return
end
local player = game.get_player(event.player_index)
get_script_data().show_other_versions[player.name] = element.state or nil
open_browser(player)
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