Move spectate into exp_scenario

The spectate and follow module moves to exp_scenario/module/control/spectate.lua
in the event_handler shape the other control modules use, and the copy in
exp_legacy is removed. Follow state is a typed record rather than a
positional table, the follow label caption is localised, and the escape
handler marks the record to stop on the next tick instead of poking an
invalid position into it.

The LuaPlayer.close_map call is gone: it does not exist in Factorio 2.0,
where the map is the remote controller and set_controller replaces it.
This commit is contained in:
bbassie
2026-09-05 19:20:55 +00:00
parent 810269971b
commit 8014cb1e77
5 changed files with 178 additions and 174 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ Adds commands relating to spectate and follow
]]
local Commands = require("modules/exp_commands")
local Spectate = require("modules.exp_legacy.modules.control.spectate") --- @dep modules.control.spectate
local Spectate = require("modules/exp_scenario/control/spectate")
--- Toggles spectator mode for the caller
Commands.new("spectate", { "exp-commands_spectate.description-spectate" })
+1
View File
@@ -65,6 +65,7 @@ add(require("modules/exp_scenario/control/report_jail"))
add(require("modules/exp_scenario/control/research"))
add(require("modules/exp_scenario/control/roles"))
add(require("modules/exp_scenario/control/spawn_area"))
add(require("modules/exp_scenario/control/spectate"))
add(require("modules/exp_scenario/control/station_auto_name"))
--- Guis
+173
View File
@@ -0,0 +1,173 @@
--[[-- Control - Spectate
Lets players spectate without losing their character, and follow other players or entities
]]
local Storage = require("modules/exp_util/storage")
local Gui = require("modules/exp_gui")
--- @class ExpScenario_Spectate
local Spectate = {
--- @package
events = {},
}
--- @class ExpScenario_Spectate.Following
--- @field player LuaPlayer
--- @field target LuaPlayer | LuaEntity
--- @field position MapPosition Where the player was after the last update, moving away stops following
--- @field started_spectate boolean True when start_follow put the player into spectator mode
--- @field stop boolean? Set when following must end on the next tick
local following = {} --- @type table<uint, ExpScenario_Spectate.Following> Keyed by player index
local spectating = {} --- @type table<uint, LuaEntity> The character a player returns to, keyed by player index
Storage.register({
following = following,
spectating = spectating,
}, function(tbl)
following = tbl.following
spectating = tbl.spectating
end)
--- Label shown while following, clicking it or pressing escape stops following
--- @class ExpScenario_Spectate.follow_label: ExpElement
--- @overload fun(parent: LuaGuiElement, target: LuaPlayer | LuaEntity): LuaGuiElement
local follow_label = Gui.define("spectate/follow_label")
:draw(function(_, parent, target)
Gui.destroy_if_valid(parent.follow_label)
local label = parent.add{
type = "label",
name = "follow_label",
style = "frame_title",
caption = { "exp_spectate.follow-label", target.name },
}
local player = Gui.get_player(parent)
local res = player.display_resolution
label.location = { 0, res.height - 150 }
label.style.width = res.width
label.style.horizontal_align = "center"
player.opened = label
return label
end)
:on_click(function(_, player)
Spectate.stop_follow(player)
end)
:on_closed(function(_, player)
-- set_controller can not be called during on_gui_closed, so the next update stops following
local data = following[player.index]
if data then data.stop = true end
end) --[[@as any]]
--- Check if a player is in spectator mode
--- @param player LuaPlayer
--- @return boolean
function Spectate.is_spectating(player)
return player.controller_type == defines.controllers.spectator
end
--- Put a player into spectator mode while keeping their character
--- @param player LuaPlayer
--- @return boolean # False when the player was already spectating or has no character
function Spectate.start_spectate(player)
if spectating[player.index] or not player.character then return false end
local character = player.character
local opened = player.opened
player.set_controller{ type = defines.controllers.spectator }
player.associate_character(character)
spectating[player.index] = character
if opened then player.opened = opened end -- Changing controller closes the opened gui
return true
end
--- Return a player to their character, or respawn them if it was killed
--- @param player LuaPlayer
function Spectate.stop_spectate(player)
local character = spectating[player.index]
spectating[player.index] = nil
if character and character.valid then
local opened = player.opened
player.teleport(character.position, character.surface)
player.set_controller{ type = defines.controllers.character, character = character }
if opened then player.opened = opened end -- Changing controller closes the opened gui
else
player.ticks_to_respawn = 300
end
end
--- Check if a player is following something
--- @param player LuaPlayer
--- @return boolean
function Spectate.is_following(player)
return following[player.index] ~= nil
end
--- Put a player into spectator mode and keep their camera on a target as it moves
--- @param player LuaPlayer
--- @param target LuaPlayer | LuaEntity
function Spectate.start_follow(player, target)
local started_spectate = Spectate.start_spectate(player)
follow_label(player.gui.screen, target)
player.teleport(target.position, target.surface)
following[player.index] = {
player = player,
target = target,
position = player.position,
started_spectate = started_spectate,
}
end
--- Give a player their camera back, returning them to their character if start_follow took it
--- @param player LuaPlayer
function Spectate.stop_follow(player)
local data = following[player.index]
if data and data.started_spectate and Spectate.is_spectating(player) then
Spectate.stop_spectate(player)
end
Gui.destroy_if_valid(player.gui.screen.follow_label)
following[player.index] = nil
end
--- Move a following player onto their target, or stop following when they have moved away or regained a character
--- @param data ExpScenario_Spectate.Following
local function update_following(data)
local player, target = data.player, data.target
local position = player.position
if data.stop or player.character or not target.valid
or position.x ~= data.position.x or position.y ~= data.position.y
then
Spectate.stop_follow(player)
else
player.teleport(target.position, target.surface)
data.position = player.position
end
end
local function on_tick()
for _, data in pairs(following) do
update_following(data)
end
end
--- Stop following when a player leaves, and stop anyone who was following them
--- @param event EventData.on_pre_player_left_game
local function on_pre_player_left_game(event)
local player = game.players[event.player_index]
Spectate.stop_follow(player)
for _, data in pairs(following) do
if data.target == player then
Spectate.stop_follow(data.player)
end
end
end
local e = defines.events
Spectate.events[e.on_tick] = on_tick
Spectate.events[e.on_pre_player_left_game] = on_pre_player_left_game
return Spectate
+3
View File
@@ -577,3 +577,6 @@ chat-jailed=__1__ was jailed because they removed too many protected entities. P
[exp_report-jail]
chat-jailed=__1__ was jailed because they were reported too many times. Please wait for a moderator.
[exp_spectate]
follow-label=Following __1__.\nClick here or press escape to stop following.