diff --git a/config/_file_loader.lua b/config/_file_loader.lua index 5df26bbd..563abc88 100644 --- a/config/_file_loader.lua +++ b/config/_file_loader.lua @@ -30,6 +30,7 @@ return { 'modules.commands.home', 'modules.commands.connect', 'modules.commands.last-location', + 'modules.commands.spectate', 'modules.commands.search', --- Addons diff --git a/config/expcore/roles.lua b/config/expcore/roles.lua index ee4f528c..6788525a 100644 --- a/config/expcore/roles.lua +++ b/config/expcore/roles.lua @@ -103,6 +103,8 @@ Roles.new_role('Trainee','TrMod') 'command/unjail', 'command/kick', 'command/ban', + 'command/spectate', + 'command/follow', 'command/search', 'command/search-amount', 'command/search-recent', @@ -120,6 +122,8 @@ Roles.new_role('Board Member','Board') :allow{ 'command/goto', 'command/repair', + 'command/spectate', + 'command/follow', } Roles.new_role('Senior Backer','Backer') diff --git a/locale/en/commands.cfg b/locale/en/commands.cfg index 37a4bc74..b4e21630 100644 --- a/locale/en/commands.cfg +++ b/locale/en/commands.cfg @@ -85,6 +85,9 @@ none-matching=No servers were found with that name, if you used an address pleas [expcom-lastlocation] response=Last location of __1__ was [gps=__2__,__3__] +[expcom-spectate] +follow-self=You can not follow yourself + [expcom-admin-marker] exit=You have left admin marker mode, all new markers will not be protected. enter=You have entered admin marker mode, all new markers will be protected. diff --git a/modules/commands/spectate.lua b/modules/commands/spectate.lua new file mode 100644 index 00000000..aa74d591 --- /dev/null +++ b/modules/commands/spectate.lua @@ -0,0 +1,33 @@ +--[[-- Commands Module - Spectate + - Adds commands relating to spectate and follow + @commands Spectate +]] + +local Spectate = require 'modules.control.spectate' --- @dep modules.control.spectate +local Commands = require 'expcore.commands' --- @dep expcore.commands +require 'config.expcore.command_general_parse' + +--- Toggles spectator mode for the caller +-- @command spectate +Commands.new_command('spectate', 'Toggles spectator mode') +:register(function(player) + if Spectate.is_spectating(player) then + Spectate.stop_spectate(player) + else + Spectate.start_spectate(player) + end +end) + +--- Enters follow mode for the caller, following the given player. +-- @command follow +-- @tparam LuaPlayer player The player that will be followed +Commands.new_command('follow', 'Start following a player in spectator') +:add_alias('f') +:add_param('player', false, 'player-online') +:register(function(player, action_player) + if player == action_player then + return Commands.error{'expcom-spectate.follow-self'} + else + Spectate.start_follow(player, action_player) + end +end) \ No newline at end of file diff --git a/modules/control/spectate.lua b/modules/control/spectate.lua new file mode 100644 index 00000000..6f9944f8 --- /dev/null +++ b/modules/control/spectate.lua @@ -0,0 +1,168 @@ + +local Event = require 'utils.event' --- @dep utils.event +local Global = require 'utils.global' --- @dep utils.global +local Gui = require 'expcore.gui' --- @dep expcore.gui + +----- Locals ----- +local follow_label -- Gui constructor +local following = {} +local spectating = {} +local Public = {} + +----- Global data ----- +Global.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 + player.set_controller{ type = defines.controllers.spectator } + player.associate_character(character) + spectating[player.index] = character + 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] then Public.stop_spectate(player) end + + Gui.destroy_if_valid(player.gui.screen[follow_label.name]) + 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 +-- @element follow_label +follow_label = +Gui.element(function(event_trigger, parent, target) + Gui.destroy_if_valid(parent[event_trigger]) + + local label = parent.add{ + name = event_trigger, + type = 'label', + style = 'heading_1_label', + caption = 'Following '..target.name..'.\nClick here or press esc to stop following.' + } + + local player = Gui.get_player_from_element(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(Public.stop_follow) +:on_close(function(player) + -- 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 \ No newline at end of file