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,173 +0,0 @@
local Storage = require("modules/exp_util/storage")
local Gui = require("modules/exp_gui")
local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
----- Locals -----
local follow_label --- @type ExpElement Gui constructor
local following = {}
local spectating = {}
local Public = {}
----- Storage data -----
Storage.register({
following = following,
spectating = spectating,
}, function(tbl)
following = tbl.following
spectating = tbl.spectating
end)
----- Public Functions -----
--- Test if a player is in spectator mode
-- @tparam LuaPlayer player The player to test the controller type of
-- @treturn boolean Returns true if the player is in spectator mode
function Public.is_spectating(player)
assert(player and player.valid, "Invalid player given to follower")
return player.controller_type == defines.controllers.spectator
end
--- Puts a player into spectator mode while maintaining an association to their character
-- @tparam LuaPlayer player The player that will be placed into spectator mode
-- @treturn boolean Returns false if the player was already in spectator mode
function Public.start_spectate(player)
assert(player and player.valid, "Invalid player given to follower")
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 -- Maintain opened after controller change
return true
end
--- Return a player from spectator mode back to their character, if their character was killed then respawn them
-- @tparam LuaPlayer player The player that will leave spectator mode
function Public.stop_spectate(player)
assert(player and player.valid, "Invalid player given to follower")
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 -- Maintain opened after controller change
else
player.ticks_to_respawn = 300
end
end
--- Test if a player is in follow mode
-- @tparam LuaPlayer player The player to test the follow mode of
-- @treturn boolean Returns true if the player is in follow mode
function Public.is_following(player)
assert(player and player.valid, "Invalid player given to follower")
return following[player.index] ~= nil
end
--- Puts a player into spectator mode and follows an entity as it moves
-- @tparam LuaPlayer player The player that will follow the entity
-- @tparam ?LuaPlayer|LuaEntity entity The player or entity that will be followed
function Public.start_follow(player, entity)
assert(player and player.valid, "Invalid player given to follower")
assert(entity and entity.valid, "Invalid entity given to follower")
local spectate = Public.start_spectate(player)
player.close_map()
follow_label(player.gui.screen, entity)
player.teleport(entity.position, entity.surface)
following[player.index] = { player, entity, entity.position, spectate }
end
--- Returns camera control to the player, will return a player to their character if start_follow placed them into spectator mode
-- @tparam LuaPlayer player The player that will regain control of their camera
function Public.stop_follow(player)
assert(player and player.valid, "Invalid player given to follower")
if following[player.index] and following[player.index][4] and Public.is_spectating(player) then
Public.stop_spectate(player)
end
Gui.destroy_if_valid(player.gui.screen.follow_label)
following[player.index] = nil
end
--- Returns camera control to all players, will return a player to their character if start_follow placed them into spectator mode
function Public.stop_all()
for key, data in pairs(following) do
Public.stop_follow(data[1])
end
end
----- Gui -----
--- Label used to show that the player is following, also used to allow esc to stop following
follow_label = Gui.define("follow-label")
:draw(function(def, parent, target)
Gui.destroy_if_valid(parent.follow_label)
local label = parent.add{
type = "label",
name = "follow_label",
style = "frame_title",
caption = "Following " .. target.name .. ".\nClick here or press esc to stop following.",
}
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(def, player, element)
Public.stop_follow(player)
end)
:on_closed(function(def, player, element)
-- Don't call set_controller during on_close as it invalidates the controller
-- Setting an invalid position (as to not equal their current) will call stop_follow on the next tick
following[player.index][3] = {}
end)
----- Events -----
--- Updates the location of the player as well as doing some sanity checks
-- @tparam LuaPlayer player The player to update the position of
-- @tparam ?LuaPlayer|LuaEntity entity The player or entity being followed
local function update_player_location(player, entity, old_position)
if player.character or not entity.valid then
Public.stop_follow(player)
elseif player.position.x ~= old_position.x or player.position.y ~= old_position.y then
Public.stop_follow(player)
else
player.teleport(entity.position, entity.surface)
end
end
--- Updates the locations of all players currently following something
local function update_all()
for _, data in pairs(following) do
update_player_location(data[1], data[2], data[3])
data[3] = data[1].position
end
end
-- Update the location of all players each tick
Event.add(defines.events.on_tick, update_all)
-- Check for player leaving
Event.add(defines.events.on_pre_player_left_game, function(event)
local player = game.players[event.player_index]
Public.stop_follow(player)
for _, data in pairs(following) do
if data[2] == player then
Public.stop_follow(data[1])
end
end
end)
----- Module Return -----
return Public
+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.