Extend mod to also support invites (#15)
MAJOR MOD CHANGE As before, moderators can silently observe any player that is not observing another player. (configurable) New: Players can invite other players to observe them. Invitations can be accepted or denied. (configurable privileges, defaults to interact) All the chat-commands are configurable. Temporary privileges can be given to observers, same set for sneak-observers and invited ones or separate set.
This commit is contained in:
parent
487307fc8f
commit
1b031ebd4c
3
.github/workflows/check-release.yml
vendored
3
.github/workflows/check-release.yml
vendored
@ -8,4 +8,5 @@ jobs:
|
||||
- name: lint
|
||||
uses: Roang-zero1/factorio-mod-luacheck@master
|
||||
with:
|
||||
luacheckrc_url: https://raw.githubusercontent.com/minetest-mods/spectator_mode/master/.luacheckrc
|
||||
luacheckrc_url:
|
||||
|
||||
|
14
.github/workflows/luacheck.yml
vendored
Normal file
14
.github/workflows/luacheck.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
name: luacheck
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: apt
|
||||
run: sudo apt-get install -y luarocks
|
||||
- name: luacheck install
|
||||
run: luarocks install --local luacheck
|
||||
- name: luacheck run
|
||||
run: $HOME/.luarocks/bin/luacheck ./
|
||||
|
12
.github/workflows/mineunit.yml
vendored
Normal file
12
.github/workflows/mineunit.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
name: mineunit
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: mt-mods/mineunit-actions@v0.3
|
||||
|
24
.luacheckrc
24
.luacheckrc
@ -1,12 +1,20 @@
|
||||
unused_args = false
|
||||
allow_defined_top = true
|
||||
|
||||
read_globals = {
|
||||
"minetest",
|
||||
"vector",
|
||||
-- Exclude regression tests / unit tests
|
||||
exclude_files = {
|
||||
"**/spec/**",
|
||||
}
|
||||
|
||||
globals = {
|
||||
"default",
|
||||
"player_api",
|
||||
player_api = { fields = { "player_attached" } },
|
||||
"spectator_mode",
|
||||
}
|
||||
|
||||
read_globals = {
|
||||
-- Stdlib
|
||||
string = { fields = { "split" } },
|
||||
table = { fields = { "copy", "insert" } },
|
||||
|
||||
-- Minetest
|
||||
"minetest",
|
||||
vector = { fields = { "copy", "new", "round" } },
|
||||
beerchat = { fields = { "has_player_muted_player" } }
|
||||
}
|
||||
|
60
LICENSE
60
LICENSE
@ -1,14 +1,56 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
--
|
||||
-- MIT License for spectator_mode code.
|
||||
-- Applies to everything that is not covered with additional copyright notes.
|
||||
--
|
||||
-- Notes:
|
||||
-- See notes below MIT license, notes in source code and copyright notes for Minetest.
|
||||
--
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
MIT License
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
Copyright (c) 2015 Jean-Patrick Guerrero <jeanpatrick.guerrero@gmail.com>
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
--
|
||||
-- Minetest LGPLv2.1 license contents
|
||||
--
|
||||
|
||||
Minetest
|
||||
Copyright (C) 2010-2018 celeron55, Perttu Ahola <celeron55@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
--
|
||||
-- Additional licenses apply, see source files and subdirectories for more information.
|
||||
--
|
||||
|
||||
|
51
README.md
51
README.md
@ -1,4 +1,8 @@
|
||||
# Spectator Mode
|
||||
[![luacheck](https://github.com/minetest-mods/spectator_mode/workflows/luacheck/badge.svg)](https://github.com/minetest-mods/spectator_mode/actions)
|
||||
[![mineunit](https://github.com/minetest-mods/spectator_mode/workflows/mineunit/badge.svg)](https://github.com/minetest-mods/spectator_mode/actions)
|
||||
[![License](https://img.shields.io/badge/License-MIT%20and%20CC--BY--SA--3.0-green.svg)](LICENSE)
|
||||
[![Minetest](https://img.shields.io/badge/Minetest-5.0+-blue.svg)](https://www.minetest.net)
|
||||
|
||||
A mod for Minetest allowing you to watch other players in their 3rd person view.
|
||||
You're invisible and undetectable for the players when you're in this mode.
|
||||
@ -6,6 +10,8 @@ You're invisible and undetectable for the players when you're in this mode.
|
||||
Can be useful for admins or moderators in their task of monitoring.
|
||||
Requires the privilege `watch`.
|
||||
|
||||
Normal players can also invite others to observe them.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `player_api` (included in [`minetest_game`](https://github.com/minetest/minetest_game))
|
||||
@ -17,5 +23,46 @@ This mod requires MT 5.0.0 and above.
|
||||
|
||||
## Commands
|
||||
|
||||
`/watch <player name>`<br>
|
||||
`/unwatch` (get back to your initial position)
|
||||
All the commands can be modified in settings, here they are listed with their default names.<br>
|
||||
|
||||
`/watch <player name>` silently attach to player<br>
|
||||
`/unwatch` (get back to your initial position)<br>
|
||||
`/watchme <player name>[,<player2 name] ... playerN name]]` invite player(s) to observe caller.<br>
|
||||
`/smn` reject an invitation<br>
|
||||
`/smy` accept an invitation<br>
|
||||
|
||||
## Settings
|
||||
|
||||
All settings can be set in minetest.conf or accessed via mod with the global field of same name.<br>
|
||||
[See settingtypes.txt](settingtypes.txt)
|
||||
|
||||
## Privileges
|
||||
|
||||
Both privileges are registered if no other mod has already done so.
|
||||
|
||||
## Compatibility
|
||||
|
||||
Before sending invites, beerchat's player meta entry is checked to make sure muted players can't invite.<br>
|
||||
Other mods can override `spectator_mode.is_permited_to_invite(name_target, name_watcher)` to add own
|
||||
conditions of when who can invite whom.
|
||||
|
||||
Moderators are kept breathing when observing via '/watch' command. Other mods can override this to
|
||||
add more functionality: `spectator_mode.keep_alive(name_watcher)`.
|
||||
|
||||
`spectator_mode.on_respawnplayer(watcher)` can be overidden to adjust what happens when an attached player
|
||||
dies and respawns. Without change, the observer is detached for a split second then re-attached.
|
||||
|
||||
While attaching a player, his hud flags are mostly turned off. Other mods can override the behaviour
|
||||
with their own implementation of<br>
|
||||
`function spectator_mode.turn_off_hud_hook(player, flags, new_hud_flags)`
|
||||
- **player** The PlayerObjectRef of player that is to be attached.
|
||||
- **flags** The player's HUD-flags prior to attaching.
|
||||
- **new_hud_flags** The table that can be manipulated and will be set as new flags.
|
||||
|
||||
## Copyright
|
||||
|
||||
Original mod DWTFYW Copyright (C) 2015 Jean-Patrick Guerrero <jeanpatrick.guerrero@gmail.com>
|
||||
Since 20220217 MIT and CC-BY-SA-3.0[see LICENSE](LICENSE)
|
||||
The MIT applies to all code in this project that is not otherwise protected. [see LICENSE](LICENSE)
|
||||
The CC-BY-SA-3.0 license applies to textures and any other content in this project which is not source code.
|
||||
|
||||
|
619
init.lua
619
init.lua
@ -1,116 +1,567 @@
|
||||
local original_pos = {}
|
||||
-- NOTE: in the output texts, the names are always in double quotes because some players have
|
||||
-- names that can be confusing without the quotes.
|
||||
-- IDEA: technically it would be possible to chain observe. Would have to climb the parent tree
|
||||
-- making sure there is nothing circular happening. Including checking all the children.
|
||||
-- A lot can go wrong with that, so it has been left out for now.
|
||||
-- Another complication to this is that there are many combinations of client<->server
|
||||
-- software versions to consider.
|
||||
-- IDEA: might be nice to have a /send_home (<player>|all) command for invitie to detach
|
||||
-- invited guests again.
|
||||
-- Currently player can force detachment by logging off.
|
||||
spectator_mode = {
|
||||
version = 20220214,
|
||||
command_accept = minetest.settings:get('spectator_mode.command_accept') or 'smy',
|
||||
command_deny = minetest.settings:get('spectator_mode.command_deny') or 'smn',
|
||||
command_detach = minetest.settings:get('spectator_mode.command_detach') or 'unwatch',
|
||||
command_invite = minetest.settings:get('spectator_mode.command_invite') or 'watchme',
|
||||
command_attach = minetest.settings:get('spectator_mode.command_attach') or 'watch',
|
||||
invitation_timeout = tonumber(minetest.settings:get(
|
||||
'spectator_mode.invitation_timeout') or 1 * 60),
|
||||
|
||||
minetest.register_privilege("watch", {
|
||||
description = "Player can watch other players",
|
||||
give_to_singleplayer = false,
|
||||
give_to_admin = true,
|
||||
})
|
||||
keep_all_observers_alive = minetest.settings:get_bool(
|
||||
'spectator_mode.keep_all_observers_alive', false),
|
||||
|
||||
local function toggle_hud_flags(player, bool)
|
||||
priv_invite = minetest.settings:get('spectator_mode.priv_invite') or 'interact',
|
||||
priv_watch = minetest.settings:get('spectator_mode.priv_watch') or 'watch',
|
||||
}
|
||||
local sm = spectator_mode
|
||||
do
|
||||
local temp = minetest.settings:get('spectator_mode.extra_observe_privs') or ''
|
||||
sm.extra_observe_privs, sm.extra_observe_privs_moderator = {}, nil
|
||||
for _, priv in ipairs(temp:split(',')) do
|
||||
sm.extra_observe_privs[priv] = true
|
||||
end
|
||||
temp = minetest.settings:get('spectator_mode.extra_observe_privs_moderator') or ''
|
||||
if temp == '' then
|
||||
-- if no extra settings for moderators are set, then the table for observers
|
||||
-- is linked and both use the same table reference.
|
||||
sm.extra_observe_privs_moderator = sm.extra_observe_privs
|
||||
-- if you prefer to keep the lists separate, uncomment next line
|
||||
--sm.extra_observe_privs_moderator = table.copy(sm.extra_observe_privs)
|
||||
else
|
||||
sm.extra_observe_privs_moderator = {}
|
||||
for _, priv in ipairs(temp:split(',')) do
|
||||
sm.extra_observe_privs_moderator[priv] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if minetest.global_exists('beerchat') then
|
||||
if 'function' == type(beerchat.has_player_muted_player) then
|
||||
sm.beerchat_has_muted = beerchat.has_player_muted_player
|
||||
end
|
||||
end
|
||||
|
||||
-- cache of saved states indexed by player name
|
||||
-- original_state['watcher'] = state
|
||||
local original_state = {}
|
||||
|
||||
-- hash-table of pending invites
|
||||
-- invites['invited_player'] = 'inviting_player'
|
||||
local invites = {}
|
||||
|
||||
-- hash-table for accepted invites.
|
||||
-- Used to determine whether watched gets notifiction when watcher detaches
|
||||
-- invited['invited_player'] = 'inviting_player'
|
||||
local invited = {}
|
||||
|
||||
|
||||
-- register privs after all mods have loaded as user may want to reuse other privs
|
||||
minetest.register_on_mods_loaded(function()
|
||||
if not minetest.registered_privileges[sm.priv_watch] then
|
||||
minetest.register_privilege(sm.priv_watch, {
|
||||
description = 'Player can watch other players.',
|
||||
give_to_singleplayer = false,
|
||||
give_to_admin = true,
|
||||
})
|
||||
end
|
||||
|
||||
if not minetest.registered_privileges[sm.priv_invite] then
|
||||
minetest.register_privilege(sm.priv_invite, {
|
||||
description = 'Player can invite other players to watch them.',
|
||||
give_to_singleplayer = false,
|
||||
give_to_admin = true,
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
-- TODO: consider making this public
|
||||
local function original_state_get(player)
|
||||
if not player or not player:is_player() then return end
|
||||
|
||||
-- check cache
|
||||
local state = original_state[player:get_player_name()]
|
||||
if state then return state end
|
||||
|
||||
-- fallback to player's meta
|
||||
return minetest.deserialize(player:get_meta():get_string('spectator_mode:state'))
|
||||
end -- original_state_get
|
||||
|
||||
|
||||
local function original_state_set(player, state)
|
||||
if not player or not player:is_player() then return end
|
||||
|
||||
-- save to cache
|
||||
original_state[player:get_player_name()] = state
|
||||
|
||||
-- backup to player's meta
|
||||
player:get_meta():set_string('spectator_mode:state', minetest.serialize(state))
|
||||
end -- original_state_set
|
||||
|
||||
|
||||
local function original_state_delete(player)
|
||||
if not player or not player:is_player() then return end
|
||||
-- remove from cache
|
||||
original_state[player:get_player_name()] = nil
|
||||
-- remove backup
|
||||
player:get_meta():set_string('spectator_mode:state', '')
|
||||
end -- original_state_delete
|
||||
|
||||
|
||||
-- keep moderators alive when they used '/watch' command
|
||||
-- overridable as servers may want to change this
|
||||
function spectator_mode.keep_alive(name_watcher)
|
||||
local watcher = minetest.get_player_by_name(name_watcher)
|
||||
if not watcher then return end -- logged off
|
||||
|
||||
-- still attached?
|
||||
if not original_state[name_watcher] then return end
|
||||
|
||||
-- has enough air? (avoid showing bubbles when not needed)
|
||||
if 8 > watcher:get_breath() then
|
||||
watcher:set_breath(9)
|
||||
end
|
||||
minetest.after(5, sm.keep_alive, name_watcher)
|
||||
end -- keep_alive
|
||||
|
||||
|
||||
-- can be overriden to manipulate new_hud_flags
|
||||
-- flags are the current hud_flags of player
|
||||
-- luacheck: no unused args
|
||||
function spectator_mode.turn_off_hud_hook(player, flags, new_hud_flags)
|
||||
new_hud_flags.breathbar = flags.breathbar
|
||||
new_hud_flags.healthbar = flags.healthbar
|
||||
end -- turn_off_hud_hook
|
||||
-- luacheck: unused args
|
||||
|
||||
|
||||
-- this doesn't hide /postool hud, hunger bar and similar
|
||||
local function turn_off_hud_flags(player)
|
||||
local flags = player:hud_get_flags()
|
||||
local new_hud_flags = {}
|
||||
|
||||
for flag in pairs(flags) do
|
||||
new_hud_flags[flag] = bool
|
||||
new_hud_flags[flag] = false
|
||||
end
|
||||
|
||||
sm.turn_off_hud_hook(player, flags, new_hud_flags)
|
||||
player:hud_set_flags(new_hud_flags)
|
||||
end
|
||||
end -- turn_off_hud_flags
|
||||
|
||||
local function unwatching(name)
|
||||
local watcher = minetest.get_player_by_name(name)
|
||||
local privs = minetest.get_player_privs(name)
|
||||
|
||||
if watcher and default.player_attached[name] == true then
|
||||
watcher:set_detach()
|
||||
player_api.player_attached[name] = false
|
||||
watcher:set_eye_offset(vector.new(), vector.new())
|
||||
watcher:set_nametag_attributes({color = {a = 255, r = 255, g = 255, b = 255}})
|
||||
-- called by the detach command '/unwatch'
|
||||
-- called on logout if player is attached at that time
|
||||
-- called before attaching to another player
|
||||
local function detach(name_watcher)
|
||||
-- nothing to do
|
||||
if not player_api.player_attached[name_watcher] then return end
|
||||
|
||||
toggle_hud_flags(watcher, true)
|
||||
local watcher = minetest.get_player_by_name(name_watcher)
|
||||
if not watcher then return end -- shouldn't ever happen
|
||||
|
||||
watcher:set_properties({
|
||||
visual_size = {x = 1, y = 1},
|
||||
makes_footstep_sound = true,
|
||||
collisionbox = {-0.3, 0.0, -0.3, 0.3, 1.7, 0.3}
|
||||
})
|
||||
watcher:set_detach()
|
||||
player_api.player_attached[name_watcher] = false
|
||||
watcher:set_eye_offset()
|
||||
|
||||
if not privs.interact and privs.watch == true then
|
||||
privs.interact = true
|
||||
minetest.set_player_privs(name, privs)
|
||||
end
|
||||
local state = original_state_get(watcher)
|
||||
-- nothing else to do
|
||||
if not state then return end
|
||||
|
||||
local pos = original_pos[name]
|
||||
if pos then
|
||||
-- set_pos seems to be very unreliable
|
||||
-- this workaround helps though
|
||||
minetest.after(0.1, function()
|
||||
watcher:set_pos(pos)
|
||||
end)
|
||||
original_pos[name] = nil
|
||||
end
|
||||
-- NOTE: older versions of MT/MC may not have this
|
||||
watcher:set_nametag_attributes({
|
||||
color = state.nametag.color,
|
||||
bgcolor = state.nametag.bgcolor
|
||||
})
|
||||
watcher:hud_set_flags(state.hud_flags)
|
||||
watcher:set_properties({
|
||||
visual_size = state.visual_size,
|
||||
makes_footstep_sound = state.makes_footstep_sound,
|
||||
collisionbox = state.collisionbox,
|
||||
})
|
||||
|
||||
-- restore privs
|
||||
local privs = minetest.get_player_privs(name_watcher)
|
||||
privs.interact = state.priv_interact
|
||||
local privs_extra = invited[name_watcher] and sm.extra_observe_privs
|
||||
or sm.extra_observe_privs_moderator
|
||||
|
||||
for key, _ in pairs(privs_extra) do
|
||||
privs[key] = state.privs_extra[key]
|
||||
end
|
||||
end
|
||||
minetest.set_player_privs(name_watcher, privs)
|
||||
|
||||
minetest.register_chatcommand("watch", {
|
||||
params = "<to_name>",
|
||||
description = "Watch a given player",
|
||||
privs = {watch = true},
|
||||
func = function(name_watcher, name_target)
|
||||
-- set_pos seems to be very unreliable
|
||||
-- this workaround helps though
|
||||
minetest.after(0.1, function()
|
||||
watcher:set_pos(state.pos)
|
||||
-- delete state only after actually moved.
|
||||
-- this helps re-attach after log-off/server crash
|
||||
original_state_delete(watcher)
|
||||
end)
|
||||
|
||||
-- if watcher was invited, notify invitee that watcher has detached
|
||||
if invited[name_watcher] then
|
||||
invited[name_watcher] = nil
|
||||
minetest.chat_send_player(state.target,
|
||||
'"' .. name_watcher .. '" has stopped looking over your shoulder.')
|
||||
|
||||
end
|
||||
minetest.log('action', '[spectator_mode] "' .. name_watcher
|
||||
.. '" detached from "' .. state.target .. '"')
|
||||
|
||||
end -- detach
|
||||
|
||||
|
||||
-- both players are online and all checks have been done when this
|
||||
-- method is called
|
||||
local function attach(name_watcher, name_target)
|
||||
|
||||
-- detach from cart, horse, bike etc.
|
||||
detach(name_watcher)
|
||||
|
||||
local watcher = minetest.get_player_by_name(name_watcher)
|
||||
local privs_watcher = minetest.get_player_privs(name_watcher)
|
||||
-- back up some attributes
|
||||
local properties = watcher:get_properties()
|
||||
local state = {
|
||||
collisionbox = properties.collisionbox,
|
||||
hud_flags = watcher:hud_get_flags(),
|
||||
makes_footstep_sound = properties.makes_footstep_sound,
|
||||
nametag = watcher:get_nametag_attributes(),
|
||||
pos = watcher:get_pos(),
|
||||
priv_interact = privs_watcher.interact,
|
||||
privs_extra = {},
|
||||
target = name_target,
|
||||
visual_size = properties.visual_size,
|
||||
}
|
||||
local privs_extra
|
||||
if invites[name_watcher] then
|
||||
privs_extra = sm.extra_observe_privs
|
||||
else
|
||||
-- wasn't invited -> '/watch' used by moderator
|
||||
privs_extra = sm.extra_observe_privs_moderator
|
||||
end
|
||||
|
||||
for key, _ in pairs(privs_extra) do
|
||||
state.privs_extra[key] = privs_watcher[key]
|
||||
privs_watcher[key] = true
|
||||
end
|
||||
original_state_set(watcher, state)
|
||||
|
||||
-- set some attributes
|
||||
turn_off_hud_flags(watcher)
|
||||
watcher:set_properties({
|
||||
visual_size = { x = 0, y = 0 },
|
||||
makes_footstep_sound = false,
|
||||
collisionbox = { 0 }, -- TODO: is this the proper/best way?
|
||||
})
|
||||
watcher:set_nametag_attributes({ color = { a = 0 }, bgcolor = { a = 0 } })
|
||||
local eye_pos = vector.new(0, -5, -20)
|
||||
watcher:set_eye_offset(eye_pos)
|
||||
-- make sure watcher can't interact
|
||||
privs_watcher.interact = nil
|
||||
minetest.set_player_privs(name_watcher, privs_watcher)
|
||||
-- and attach
|
||||
player_api.player_attached[name_watcher] = true
|
||||
local target = minetest.get_player_by_name(name_target)
|
||||
watcher:set_attach(target, '', eye_pos)
|
||||
minetest.log('action', '[spectator_mode] "' .. name_watcher
|
||||
.. '" attached to "' .. name_target .. '"')
|
||||
|
||||
if sm.keep_all_observers_alive or (not invites[name_watcher]) then
|
||||
-- server keeps all observers alive
|
||||
-- or moderator used '/watch' to sneak up without invite
|
||||
minetest.after(3, sm.keep_alive, name_watcher)
|
||||
end
|
||||
end -- attach
|
||||
|
||||
|
||||
-- called by '/watch' command
|
||||
local function watch(name_watcher, name_target)
|
||||
if original_state[name_watcher] then
|
||||
return true, 'You are currently watching "'
|
||||
.. original_state[name_watcher].target
|
||||
.. '". Say /' .. sm.command_detach .. ' first.'
|
||||
|
||||
end
|
||||
if name_watcher == name_target then
|
||||
return true, 'You may not watch yourself.'
|
||||
end
|
||||
|
||||
local target = minetest.get_player_by_name(name_target)
|
||||
if not target then
|
||||
return true, 'Invalid target name "' .. name_target .. '"'
|
||||
end
|
||||
|
||||
-- avoid infinite loops
|
||||
if original_state[name_target] then
|
||||
return true, '"' .. name_target .. '" is watching "'
|
||||
.. original_state[name_target].target .. '". You may not watch a watcher.'
|
||||
end
|
||||
|
||||
attach(name_watcher, name_target)
|
||||
return true, 'Watching "' .. name_target .. '" at '
|
||||
.. minetest.pos_to_string(vector.round(target:get_pos()))
|
||||
|
||||
end -- watch
|
||||
|
||||
|
||||
local function invite_timed_out(name_watcher)
|
||||
-- did the watcher already accept/decline?
|
||||
if not invites[name_watcher] then return end
|
||||
|
||||
minetest.chat_send_player(invites[name_watcher],
|
||||
'Invitation to "' .. name_watcher .. '" timed-out.')
|
||||
|
||||
minetest.chat_send_player(name_watcher,
|
||||
'Invitation from "' .. invites[name_watcher] .. '" timed-out.')
|
||||
|
||||
invites[name_watcher] = nil
|
||||
end -- invite_timed_out
|
||||
|
||||
|
||||
-- called by '/watchme' command
|
||||
local function watchme(name_target, param)
|
||||
if original_state[name_target] then
|
||||
return true, 'You are watching "' .. original_state[name_target].target
|
||||
.. '", no chain watching is allowed.'
|
||||
end
|
||||
|
||||
if '' == param then
|
||||
return true, 'Please provide at least one player name.'
|
||||
end
|
||||
|
||||
local messages = {}
|
||||
local count_invites = 0
|
||||
local invitation_timeout_string = tostring(sm.invitation_timeout)
|
||||
local invitation_postfix = '" has invited you to observe them. '
|
||||
.. 'Accept with /' .. sm.command_accept
|
||||
.. ', deny with /' .. sm.command_deny .. '.\n'
|
||||
.. 'The invite expires in ' .. invitation_timeout_string .. ' seconds.'
|
||||
|
||||
-- checks whether watcher may be invited by target and returns error message if not
|
||||
-- if permitted, invites watcher and returns success message
|
||||
local function invite(name_watcher)
|
||||
if name_watcher == name_target then
|
||||
return true, "You may not watch yourself"
|
||||
return 'You may not watch yourself.'
|
||||
end
|
||||
|
||||
local target = minetest.get_player_by_name(name_target)
|
||||
|
||||
if not target then
|
||||
return true, "Unknown target player name"
|
||||
if original_state[name_watcher] then
|
||||
return '"' .. name_watcher .. '" is busy watching another player.'
|
||||
end
|
||||
|
||||
-- avoid infinite loops
|
||||
if original_pos[name_target] then
|
||||
return true, name_target .. " is already watching a player."
|
||||
if invites[name_watcher] then
|
||||
return '"' .. name_watcher .. '" has a pending invite, try again later.'
|
||||
end
|
||||
|
||||
local watcher = minetest.get_player_by_name(name_watcher)
|
||||
local privs_watcher = minetest.get_player_privs(name_watcher)
|
||||
|
||||
if player_api.player_attached[name_watcher] == true then
|
||||
unwatching(name_watcher)
|
||||
if not minetest.get_player_by_name(name_watcher) then
|
||||
return '"' .. name_watcher .. '" is not online.'
|
||||
end
|
||||
original_pos[name_watcher] = watcher:get_pos()
|
||||
|
||||
player_api.player_attached[name_watcher] = true
|
||||
watcher:set_attach(target, "", vector.new(0, -5, -20), vector.new())
|
||||
watcher:set_eye_offset(vector.new(0, -5, -20), vector.new())
|
||||
watcher:set_nametag_attributes({color = {a = 0}})
|
||||
if not sm.is_permited_to_invite(name_target, name_watcher) then
|
||||
return 'You may not invite "' .. name_watcher .. '".'
|
||||
end
|
||||
|
||||
toggle_hud_flags(watcher, true)
|
||||
count_invites = count_invites + 1
|
||||
invites[name_watcher] = name_target
|
||||
minetest.after(sm.invitation_timeout, invite_timed_out, name_watcher)
|
||||
-- notify invited
|
||||
minetest.chat_send_player(name_watcher, '"' .. name_target .. invitation_postfix)
|
||||
|
||||
watcher:set_properties({
|
||||
visual_size = {x = 0, y = 0},
|
||||
makes_footstep_sound = false,
|
||||
collisionbox = {0}
|
||||
})
|
||||
|
||||
privs_watcher.interact = nil
|
||||
minetest.set_player_privs(name_watcher, privs_watcher)
|
||||
|
||||
return true, 'Watching "' .. name_target .. '" at '
|
||||
.. minetest.pos_to_string(vector.round(target:get_pos()))
|
||||
-- notify invitee
|
||||
return 'You have invited "' .. name_watcher .. '".'
|
||||
end -- invite()
|
||||
|
||||
for name_watcher in string.gmatch(param, '[^%s,]+') do
|
||||
table.insert(messages, invite(name_watcher))
|
||||
end
|
||||
-- notify invitee
|
||||
local text = table.concat(messages, '\n')
|
||||
if 0 < count_invites then
|
||||
text = text .. '\nThe invitations expire in '
|
||||
.. invitation_timeout_string .. ' seconds.'
|
||||
end
|
||||
return true, text
|
||||
end -- watchme
|
||||
|
||||
|
||||
-- this function only checks privs etc. Mechanics are already checked in watchme()
|
||||
-- other mods can override and extend these checks
|
||||
function spectator_mode.is_permited_to_invite(name_target, name_watcher)
|
||||
if minetest.get_player_privs(name_target)[sm.priv_watch] then
|
||||
return true
|
||||
end
|
||||
|
||||
if not minetest.get_player_privs(name_target)[sm.priv_invite] then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check for beerchat mute/ignore
|
||||
if sm.beerchat_has_muted and sm.beerchat_has_muted(name_watcher, name_target) then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end -- is_permited_to_invite
|
||||
|
||||
|
||||
-- called by the accept command '/smy'
|
||||
local function accept_invite(name_watcher)
|
||||
local name_target = invites[name_watcher]
|
||||
if not name_target then
|
||||
return true, 'There is no invite for you. Maybe it timed-out.'
|
||||
end
|
||||
|
||||
attach(name_watcher, name_target)
|
||||
invites[name_watcher] = nil
|
||||
invited[name_watcher] = name_target
|
||||
minetest.chat_send_player(name_target,
|
||||
'"' .. name_watcher .. '" is now attached to you.')
|
||||
|
||||
return true, 'OK, you have been attached to "' .. name_target .. '". To disable type /'
|
||||
.. sm.command_detach
|
||||
|
||||
end -- accept_invite
|
||||
|
||||
|
||||
-- called by the deny command '/smn'
|
||||
local function decline_invite(name_watcher)
|
||||
if not invites[name_watcher] then
|
||||
return true, 'There is no invite for you. Maybe it timed-out.'
|
||||
end
|
||||
|
||||
minetest.chat_send_player(invites[name_watcher],
|
||||
'"' .. name_watcher .. '" declined the invite.')
|
||||
|
||||
invites[name_watcher] = nil
|
||||
return true, 'OK, declined invite.'
|
||||
end -- decline_invite
|
||||
|
||||
|
||||
local function unwatch(name_watcher)
|
||||
-- nothing to do
|
||||
if not player_api.player_attached[name_watcher] then
|
||||
return true, 'You are not observing anybody.'
|
||||
end
|
||||
|
||||
detach(name_watcher)
|
||||
return true -- no message as that has been sent by detach()
|
||||
end -- unwatch
|
||||
|
||||
|
||||
local function on_joinplayer(watcher)
|
||||
local state = original_state_get(watcher)
|
||||
if not state then return end
|
||||
|
||||
-- attempt to move to original state after log-off
|
||||
-- during attach or server crash
|
||||
local name_watcher = watcher:get_player_name()
|
||||
original_state[name_watcher] = state
|
||||
player_api.player_attached[name_watcher] = true
|
||||
detach(name_watcher)
|
||||
end -- on_joinplayer
|
||||
|
||||
|
||||
local function on_leaveplayer(watcher)
|
||||
local name_watcher = watcher:get_player_name()
|
||||
if invites[name_watcher] then
|
||||
-- invitation exists for leaving player
|
||||
minetest.chat_send_player(invites[name_watcher],
|
||||
'Invitation to "' .. name_watcher .. '" invalidated because of logout.')
|
||||
|
||||
invites[name_watcher] = nil
|
||||
end
|
||||
-- detach before leaving
|
||||
detach(name_watcher)
|
||||
-- detach any that are watching this user
|
||||
local attached = {}
|
||||
for name, state in pairs(original_state) do
|
||||
if name_watcher == state.target then
|
||||
table.insert(attached, name)
|
||||
end
|
||||
end
|
||||
-- we use separate loop to avoid editing a
|
||||
-- hash while it's being looped
|
||||
for _, name in ipairs(attached) do
|
||||
detach(name)
|
||||
end
|
||||
end -- on_leaveplayer
|
||||
|
||||
|
||||
-- different servers may want different behaviour, they can
|
||||
-- override this function
|
||||
function spectator_mode.on_respawnplayer(watcher)
|
||||
-- * Called when player is to be respawned
|
||||
-- * Called _before_ repositioning of player occurs
|
||||
-- * return true in func to disable regular player placement
|
||||
local state = original_state_get(watcher)
|
||||
if not state then return end
|
||||
|
||||
local name_target = state.target
|
||||
local name_watcher = watcher:get_player_name()
|
||||
player_api.player_attached[name_watcher] = true
|
||||
-- detach destroys invited entry, we need to restore that
|
||||
if invited[name_watcher] then
|
||||
detach(name_watcher)
|
||||
-- mark as invited so players get info in chat on detach.
|
||||
invited[name_watcher] = name_target
|
||||
else
|
||||
-- was a moderator using '/watch' -> conceal the spy.
|
||||
detach(name_watcher)
|
||||
end
|
||||
minetest.after(.4, attach, name_watcher, name_target)
|
||||
return true
|
||||
end -- on_respawnplayer
|
||||
|
||||
|
||||
minetest.register_chatcommand(sm.command_attach, {
|
||||
params = '<target name>',
|
||||
description = 'Watch a given player',
|
||||
privs = { [sm.priv_watch] = true },
|
||||
func = watch,
|
||||
})
|
||||
|
||||
minetest.register_chatcommand("unwatch", {
|
||||
description = "Unwatch a player",
|
||||
privs = {watch=true},
|
||||
func = function(name, param)
|
||||
unwatching(name)
|
||||
end
|
||||
|
||||
minetest.register_chatcommand(sm.command_detach, {
|
||||
description = 'Unwatch a player',
|
||||
privs = { },
|
||||
func = unwatch,
|
||||
})
|
||||
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
local name = player:get_player_name()
|
||||
unwatching(name)
|
||||
end)
|
||||
|
||||
minetest.register_chatcommand(sm.command_invite, {
|
||||
description = 'Invite player(s) to watch you',
|
||||
params = '<player name>[,<player2 name>[ <playerN name>]' .. ']',
|
||||
privs = { [sm.priv_invite] = true },
|
||||
func = watchme,
|
||||
})
|
||||
|
||||
|
||||
minetest.register_chatcommand(sm.command_accept, {
|
||||
description = 'Accept an invitation to watch another player',
|
||||
params = '',
|
||||
privs = { },
|
||||
func = accept_invite,
|
||||
})
|
||||
|
||||
|
||||
minetest.register_chatcommand(sm.command_deny, {
|
||||
description = 'Deny an invitation to watch another player',
|
||||
params = '',
|
||||
privs = { },
|
||||
func = decline_invite,
|
||||
})
|
||||
|
||||
|
||||
minetest.register_on_joinplayer(on_joinplayer)
|
||||
minetest.register_on_leaveplayer(on_leaveplayer)
|
||||
minetest.register_on_respawnplayer(spectator_mode.on_respawnplayer)
|
||||
|
||||
|
7
mod.conf
7
mod.conf
@ -1,6 +1,9 @@
|
||||
name = spectator_mode
|
||||
depends = default, player_api
|
||||
optional_depends = beerchat
|
||||
description = """
|
||||
A mod for Minetest allowing you to watch other players in their 3rd person view.
|
||||
You're invisible and undetectable for the players when you're in this mode.
|
||||
A mod for Minetest allowing players to watch other players in their 3rd person view.
|
||||
Observers are invisible and can be kept alive.
|
||||
Privileged players can observe others without invites. They can give away their presence
|
||||
by dropping items or death creating bones and visible dead player.
|
||||
"""
|
||||
|
36
settingtypes.txt
Normal file
36
settingtypes.txt
Normal file
@ -0,0 +1,36 @@
|
||||
# After an invite has successfully been sent, the watcher needs to accept with this command.
|
||||
spectator_mode.command_accept (Chatcommand to accept an invitation) string smy
|
||||
|
||||
# After an invite has successfully been sent, the watcher may decline it with this command.
|
||||
spectator_mode.command_deny (Chatcommand to deny an invitation) string smn
|
||||
|
||||
# To stop observing another player, issue this command
|
||||
spectator_mode.command_detach (Chatcommand to stop observing a player) string unwatch
|
||||
|
||||
# Additional privs granted to observers. e.g. noclip,never_hungry
|
||||
spectator_mode.extra_observe_privs (Extra privs for observers) string
|
||||
|
||||
# Additional privs granted to observers that used '/watch' command. e.g. jail,kick,teleport
|
||||
# If left empty will use spectator_mode.extra_observe_privs (same table reference).
|
||||
spectator_mode.extra_observe_privs_moderator (Extra privs for observing moderators) string
|
||||
|
||||
# To invite another player to observe player that issued this command
|
||||
spectator_mode.command_invite (Chatcommand to invite other player) string watchme
|
||||
|
||||
# To start observing another player, issue this command
|
||||
spectator_mode.command_attach (Chatcommand to start observing a player) string watch
|
||||
|
||||
# Invitations invalidate after this many seconds if they haven't been accepted or denied
|
||||
spectator_mode.invitation_timeout (Duration invites are valid for in seconds) int 60
|
||||
|
||||
# Not only moderators are kept oxygenated, but all observers when this is set to true.
|
||||
# For auto-feeding the spectator_mode.keep_alive() function needs to be overridden by
|
||||
# another mod.
|
||||
spectator_mode.keep_all_observers_alive (Keep all observers alive) bool false
|
||||
|
||||
# The priv needed to send observation invites.
|
||||
spectator_mode.priv_invite (Player priv to invite others to observe) string interact
|
||||
|
||||
# The priv needed to silently observe any player that isn't currently watching another one
|
||||
spectator_mode.priv_watch (Moderator priv to watch any player) string watch
|
||||
|
17
spec/fixtures/beerchat.lua
vendored
Normal file
17
spec/fixtures/beerchat.lua
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
-- emulating the only part of [beerchat] that this mod uses
|
||||
-- https://github.com/minetest-beerchat/beerchat/blob/69400d640c5f6972ab3c69b955b012aecba53ad5/common.lua#L56
|
||||
|
||||
mineunit:set_modpath("beerchat", "../beerchat")
|
||||
|
||||
_G.beerchat = { has_player_muted_player = function(name, other_name)
|
||||
local player = minetest.get_player_by_name(name)
|
||||
-- check jic method is used incorrectly
|
||||
if not player then
|
||||
return true
|
||||
end
|
||||
|
||||
local key = "beerchat:muted:" .. other_name
|
||||
local meta = player:get_meta()
|
||||
return "true" == meta:get_string(key)
|
||||
end }
|
||||
|
115
spec/fixtures/mineunit_extensions.lua
vendored
Normal file
115
spec/fixtures/mineunit_extensions.lua
vendored
Normal file
@ -0,0 +1,115 @@
|
||||
-- methods that mineunit has not yet implemented or will never implement but
|
||||
-- are needed by the unit tests.
|
||||
|
||||
-- not needed with core 5.5.0 (possibly earlier)
|
||||
function vector.copy(v) return { x = v.x or 0, y = v.y or 0, z = v.z or 0 } end
|
||||
|
||||
-- not needed with core 5.5.0 (possibly earlier)
|
||||
function vector.zero() return { x = 0, y = 0, z = 0 } end
|
||||
|
||||
function Player:hud_get_flags()
|
||||
return self._hud_flags or { hotbar = true, healthbar = true, crosshair = true,
|
||||
wielditem = true, breathbar = true, minimap = false, minimap_radar = false }
|
||||
end
|
||||
function Player:hud_set_flags(new_flags)
|
||||
if not self._hud_flags then self._hud_flags = self:hud_get_flags() end
|
||||
for flag, value in pairs(new_flags) do if nil ~= self._hud_flags[flag] then self._hud_flags[flag] = not not value end end
|
||||
end
|
||||
|
||||
function ObjectRef:get_nametag_attributes()
|
||||
if not self._nametag_attributes then self._nametag_attributes = {
|
||||
text = self._nametag_text or '',
|
||||
color = self._nametag_color or { a = 255, r = 255, g = 255, b = 255 },
|
||||
bgcolor = self._nametag_bgcolor or { a = 0, r = 0, g = 0, b = 0 },
|
||||
}
|
||||
end
|
||||
return self._nametag_attributes
|
||||
end
|
||||
|
||||
function Player:set_eye_offset(firstperson, thirdperson)
|
||||
self._eye_offset_first =
|
||||
firstperson and vector.copy(firstperson) or vector.zero()
|
||||
|
||||
thirdperson = thirdperson and vector.copy(thirdperson) or vector.zero()
|
||||
thirdperson.x = math.max(-10, math.min(10, thirdperson.x))
|
||||
thirdperson.y = math.max(-10, math.min(15, thirdperson.y))
|
||||
thirdperson.z = math.max(-5, math.min(5, thirdperson.z))
|
||||
self._eye_offset_third = thirdperson
|
||||
end
|
||||
|
||||
function Player:get_breath() return self._breath or 10 end
|
||||
function Player:set_breath(value) self._breath = tonumber(value) or self._breath end
|
||||
|
||||
function ObjectRef:set_nametag_attributes(new_attributes)
|
||||
if not self._nametag_attributes then self:get_nametag_attributes() end
|
||||
for key, value in pairs(new_attributes) do
|
||||
if nil ~= self._nametag_attributes[key] then
|
||||
if 'name' == key then
|
||||
self._nametag_attributes.name = tostring(value)
|
||||
else
|
||||
for subkey, subvalue in pairs(new_attributes[key]) do
|
||||
if nil ~= self._nametag_attributes[key][subkey] then
|
||||
self._nametag_attributes[key][subkey] = tonumber(subvalue)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ObjectRef:set_pos(value)
|
||||
self._pos = vector.copy(value)
|
||||
for _, child in ipairs(self:get_children()) do
|
||||
child:set_pos(vector.add(self._pos, child._attach.position))
|
||||
end
|
||||
end
|
||||
function ObjectRef:set_attach(parent, bone, position, rotation, forced_visible)
|
||||
if not parent then return end
|
||||
if self._attach and self._attach.parent == parent then
|
||||
mineunit:info('Attempt to attach to parent that object is already attached to.')
|
||||
return
|
||||
end
|
||||
-- detach if attached
|
||||
self:set_detach()
|
||||
local obj = parent
|
||||
while true do
|
||||
if not obj._attach then break end
|
||||
if obj._attach.parent == self then
|
||||
mineunit:warning('Mod bug: Attempted to attach object to an object that '
|
||||
.. 'is directly or indirectly attached to the first object. -> '
|
||||
.. 'circular attachment chain.')
|
||||
return
|
||||
end
|
||||
obj = obj._attach.parent
|
||||
end
|
||||
if 'table' ~= type(parent._children) then parent._children = {} end
|
||||
table.insert(parent._children, self)
|
||||
self._attach = {
|
||||
parent = parent,
|
||||
bone = bone or '',
|
||||
position = position or vector.zero(),
|
||||
rotation = rotation or vector.zero(),
|
||||
forced_visible = not not forced_visible,
|
||||
}
|
||||
self._pitch = self._attach.position.x
|
||||
self._roll = self._attach.position.z
|
||||
self._yaw = self._attach.position.y
|
||||
self:set_pos(vector.add(parent:get_pos(), self._attach.position))
|
||||
-- TODO: bones depending on object type
|
||||
end
|
||||
function ObjectRef:get_attach()
|
||||
return self._attach
|
||||
end
|
||||
function ObjectRef:get_children()
|
||||
return self._children or {}
|
||||
end
|
||||
function ObjectRef:set_detach()
|
||||
if not self._attach then return end
|
||||
local new_children = {}
|
||||
for _, child in ipairs(self._attach.parent._children) do
|
||||
if child ~= self then table.insert(new_children, child) end
|
||||
end
|
||||
self._attach.parent._children = new_children
|
||||
self._attach = nil
|
||||
end
|
||||
|
6
spec/fixtures/player_api.lua
vendored
Normal file
6
spec/fixtures/player_api.lua
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
-- emulating the only part of [player_api] that this mod uses
|
||||
|
||||
mineunit:set_modpath("player_api", "../player_api")
|
||||
|
||||
_G.player_api = { player_attached = {} }
|
||||
|
305
spec/init_spec.lua
Normal file
305
spec/init_spec.lua
Normal file
@ -0,0 +1,305 @@
|
||||
-- main unit testing file that mineunit picks up
|
||||
-- https://github.com/S-S-X/mineunit
|
||||
|
||||
require("mineunit")
|
||||
|
||||
mineunit("core")
|
||||
mineunit("player")
|
||||
mineunit("server")
|
||||
mineunit('common/after')
|
||||
|
||||
-- mimic player_api.player_attached
|
||||
fixture('player_api')
|
||||
-- add some not yet included functions
|
||||
fixture('mineunit_extensions')
|
||||
-- mimic beerchat.has_player_muted_player
|
||||
fixture('beerchat')
|
||||
|
||||
local function pd1(m) print(dump(m)) end
|
||||
local function pd(...) for _, m in ipairs({...}) do pd1(m) end end
|
||||
|
||||
-- override chat_send_player to inspect what was sent
|
||||
local chatlog = {}
|
||||
local core_chat_send_player = core.chat_send_player
|
||||
function core.chat_send_player(to_name, message)
|
||||
table.insert(chatlog, { to = to_name, message = message })
|
||||
return core_chat_send_player(to_name, message)
|
||||
end
|
||||
local function reset_chatlog() chatlog = {} end
|
||||
|
||||
describe("Mod initialization", function()
|
||||
|
||||
it("Wont crash", function()
|
||||
sourcefile("init")
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
describe('Watching:', function()
|
||||
|
||||
-- create some players
|
||||
local players = {
|
||||
SX = Player("SX", { interact = 1 }),
|
||||
boss = Player("boss", { interact = 1, watch = 1 }),
|
||||
dude1 = Player("dude1", { interact = 1, }),
|
||||
dude2 = Player("dude2", { interact = 1, }),
|
||||
dude3 = Player("dude3", { interact = false, }),
|
||||
}
|
||||
local start_positions = {}
|
||||
local boss = players.boss
|
||||
local dude1 = players.dude1
|
||||
local dude2 = players.dude2
|
||||
local dude3 = players.dude3
|
||||
|
||||
setup(function()
|
||||
-- make sure the privs are registered
|
||||
mineunit:mods_loaded()
|
||||
-- log on all players and move them to unique positions
|
||||
local i, pos = 1
|
||||
for name, player in pairs(players) do
|
||||
mineunit:execute_on_joinplayer(player)
|
||||
pos = vector.new(10 * i, 20 * i, 30 * i)
|
||||
start_positions[name] = pos
|
||||
player:set_pos(pos)
|
||||
i = i + 1
|
||||
end
|
||||
end)
|
||||
|
||||
teardown(function()
|
||||
mineunit:info('shutting down server')
|
||||
for _, player in pairs(players) do
|
||||
mineunit:execute_on_leaveplayer(player)
|
||||
end
|
||||
mineunit:execute_globalstep(100)
|
||||
end)
|
||||
|
||||
it("boss attaches to dude1", function()
|
||||
reset_chatlog()
|
||||
boss:send_chat_message("/watch dude1")
|
||||
assert.equals(1, #chatlog, 'unexpected amount of messages, '
|
||||
.. 'was dude1 notified by accident')
|
||||
assert.equals('boss', chatlog[1].to)
|
||||
assert.equals(1, chatlog[1].message:find('^Watching "dude1" at %('))
|
||||
local pos = boss:get_pos()
|
||||
assert.equals(start_positions.dude1.x, pos.x)
|
||||
assert.equals(start_positions.dude1.y - 5, pos.y)
|
||||
assert.equals(start_positions.dude1.z - 20, pos.z)
|
||||
end)
|
||||
|
||||
it('boss returns to start position and nobody is notified about it', function()
|
||||
-- let's make sure boss is still attached, jic we change previous test
|
||||
assert.is_false(vector.equals(start_positions.boss, boss:get_pos()),
|
||||
'boss is still at starting position: unit setup error')
|
||||
reset_chatlog()
|
||||
boss:send_chat_message('/unwatch')
|
||||
mineunit:execute_globalstep(1)
|
||||
assert.equals(0, #chatlog, 'there was an error message sent to ?')
|
||||
assert.is_true(vector.equals(start_positions.boss, boss:get_pos()),
|
||||
'boss did not move back to starting position')
|
||||
end)
|
||||
|
||||
it('boss tries to unwatch when not watching', function()
|
||||
reset_chatlog()
|
||||
boss:send_chat_message('/unwatch')
|
||||
mineunit:execute_globalstep(1)
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.is_not_nil(chatlog[1].message:find('not observing'),
|
||||
'unexpected chat response')
|
||||
end)
|
||||
|
||||
it('player receives message when issuing /unwatch while not attached', function()
|
||||
reset_chatlog()
|
||||
dude1:send_chat_message('/unwatch')
|
||||
mineunit:execute_globalstep(1)
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.is_not_nil(chatlog[1].message:find('not observing'),
|
||||
'unexpected chat response')
|
||||
end)
|
||||
|
||||
it('invitations are sent and expire', function()
|
||||
reset_chatlog()
|
||||
-- we also test multiple invites with space separation
|
||||
dude2:send_chat_message('/watchme dude1 SX')
|
||||
assert.equals('dude1', chatlog[1].to, 'dude1 did not get invited')
|
||||
assert.equals('SX', chatlog[2].to, 'SX did not get invited')
|
||||
assert.equals('dude2', chatlog[3].to, 'dude2 did not get message')
|
||||
local message = chatlog[3].message
|
||||
assert.is_true(message:find('"dude1"') and message:find('"SX"') and true,
|
||||
'response message does not contain invities')
|
||||
assert.is_not_nil(message:find('60 seconds'),
|
||||
'response message does not contain expire info')
|
||||
reset_chatlog()
|
||||
mineunit:execute_globalstep(60)
|
||||
assert.equals(4, #chatlog, 'unexpected chatlog count')
|
||||
reset_chatlog()
|
||||
players.SX:send_chat_message('/smy')
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('SX', chatlog[1].to, 'message was not sent to SX but ' .. chatlog[1].to)
|
||||
message = chatlog[1].message
|
||||
assert.is_not_nil(message:find('timed%-out%.$'),
|
||||
'time out message does not end with "timed-out."')
|
||||
local pos = players.SX:get_pos()
|
||||
assert.is_true(vector.equals(start_positions.SX, pos),
|
||||
'Invitation did not expire, SX was moved.')
|
||||
end)
|
||||
|
||||
it('watching by normal player not possible', function()
|
||||
local pos = dude2:get_pos()
|
||||
reset_chatlog()
|
||||
dude2:send_chat_message('/watch dude1')
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('dude2', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.is_not_nil(chatlog[1].message:find('to run this command'),
|
||||
'unexpected error message')
|
||||
assert.is_true(vector.equals(pos, dude2:get_pos()))
|
||||
end)
|
||||
|
||||
it('inviting is not possible when not having invite priv', function()
|
||||
local last_priv = spectator_mode.priv_invite
|
||||
-- set needed priv to avoid using dude3, this allows us to test
|
||||
-- minetest.conf settings
|
||||
spectator_mode.priv_invite = 'testPrivThatDoesNotExist'
|
||||
reset_chatlog()
|
||||
-- we also test comma list
|
||||
dude2:send_chat_message('/watchme dude1,SX')
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
local message = chatlog[1].message
|
||||
assert.is_not_nil(message:find('"dude1"'), 'did not parse player list correctly')
|
||||
assert.is_not_nil(message:find('"SX"'), 'did not parse player list correctly')
|
||||
assert.is_not_nil(message:find('^You may not invite'), 'did not revoke inivting')
|
||||
assert.is_nil(message:find('60 seconds'), 'time out message was wrongfully added')
|
||||
-- restore priv setting
|
||||
spectator_mode.priv_invite = last_priv
|
||||
end)
|
||||
|
||||
it('player can not invite himself', function()
|
||||
reset_chatlog()
|
||||
dude1:send_chat_message('/watchme dude1')
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('dude1', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.is_not_nil(chatlog[1].message:find('may not watch yourself'),
|
||||
'unexpected chat response')
|
||||
end)
|
||||
|
||||
it('moderator can not invite himself', function()
|
||||
reset_chatlog()
|
||||
boss:send_chat_message('/watchme boss')
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('boss', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.is_not_nil(chatlog[1].message:find('may not watch yourself'),
|
||||
'unexpected chat response')
|
||||
end)
|
||||
|
||||
it('moderator can not attach to an observing player and gets name of observed player', function()
|
||||
dude1:send_chat_message('/watchme dude2')
|
||||
dude2:send_chat_message('/smy')
|
||||
mineunit:execute_globalstep(1)
|
||||
reset_chatlog()
|
||||
boss:send_chat_message('/watch dude2')
|
||||
mineunit:execute_globalstep(1)
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('boss', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.is_not_nil(chatlog[1].message:find('^"dude2" is watching "dude1"'),
|
||||
'unexpected chat response')
|
||||
end)
|
||||
it('player can not invite an observing player', function()
|
||||
reset_chatlog()
|
||||
players.SX:send_chat_message('/watchme dude2')
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('SX', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.is_not_nil(chatlog[1].message:find('^"dude2".*watching another player%.$'),
|
||||
'unexpected chat response')
|
||||
|
||||
end)
|
||||
-- TODO: check if this can't be exploited to make circular attaching
|
||||
it('player can invite an observed player', function()
|
||||
reset_chatlog()
|
||||
players.SX:send_chat_message('/watchme dude1')
|
||||
assert.equals(2, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('dude1', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.equals('SX', chatlog[2].to, 'unexpected recipient.')
|
||||
assert.is_not_nil(chatlog[1].message:find('^"SX".* seconds%.$'),
|
||||
'unexpected chat response')
|
||||
end)
|
||||
it('player can deny an invite', function()
|
||||
reset_chatlog()
|
||||
dude1:send_chat_message('/smn')
|
||||
assert.equals(2, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('SX', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.equals('dude1', chatlog[2].to, 'unexpected recipient.')
|
||||
end)
|
||||
it('player can detach and returns to original position', function()
|
||||
reset_chatlog()
|
||||
dude2:send_chat_message('/unwatch')
|
||||
mineunit:execute_globalstep(1)
|
||||
assert.is_true(vector.equals(start_positions.dude2, dude2:get_pos()),
|
||||
'dude2 did not move back to starting position')
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('dude1', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.is_not_nil(chatlog[1].message:find('^"dude2" has stopped loo'),
|
||||
'unexpected chat response')
|
||||
end)
|
||||
it('can not invite a player with pending invitation', function()
|
||||
dude1:send_chat_message('/watchme dude2')
|
||||
reset_chatlog()
|
||||
players.SX:send_chat_message('/watchme dude2')
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('SX', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.is_not_nil(chatlog[1].message:find('^"dude2" has a pen'),
|
||||
'unexpected chat response')
|
||||
end)
|
||||
it('boss can attach to an unwatched player with pending invitation', function()
|
||||
reset_chatlog()
|
||||
boss:send_chat_message('/watch dude2')
|
||||
mineunit:execute_globalstep(1)
|
||||
assert.is_true(vector.equals(boss:get_pos(), vector.add(
|
||||
dude2:get_pos(), vector.new(0, -5, -20))), 'boss not with dude2')
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('boss', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.is_not_nil(chatlog[1].message:find('^Watching "dude2"'),
|
||||
'unexpected chat response')
|
||||
end)
|
||||
it('can accept invitation after a moderator also attached', function()
|
||||
reset_chatlog()
|
||||
dude2:send_chat_message('/smy')
|
||||
mineunit:execute_globalstep(1)
|
||||
assert.equals(2, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('dude1', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.equals('dude2', chatlog[2].to, 'unexpected recipient.')
|
||||
assert.is_not_nil(chatlog[1].message:find('^"dude2" is now attached'),
|
||||
'unexpected chat response')
|
||||
assert.is_not_nil(chatlog[2].message:find('^OK, you have been atta'),
|
||||
'unexpected chat response')
|
||||
end)
|
||||
it('dude2 is detached when dude1 logs off', function()
|
||||
reset_chatlog()
|
||||
mineunit:execute_on_leaveplayer(dude1)
|
||||
mineunit:execute_globalstep(1)
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('dude1', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.is_not_nil(chatlog[1].message:find('^"dude2" has stopped loo'),
|
||||
'unexpected chat response')
|
||||
end)
|
||||
it('dude3 ignores dude1 and can not be invited by dude1', function()
|
||||
reset_chatlog()
|
||||
dude3:get_meta():set_string('beerchat:muted:dude1', 'true')
|
||||
dude1:send_chat_message('/watchme dude3')
|
||||
assert.equals(1, #chatlog, 'unexpected chatlog count')
|
||||
assert.equals('dude1', chatlog[1].to, 'unexpected recipient.')
|
||||
assert.equals(chatlog[1].message, 'You may not invite "dude3".')
|
||||
end)
|
||||
it('boss is detached when he logs on after logging off while attached', function()
|
||||
reset_chatlog()
|
||||
assert.is_true(vector.equals(boss:get_pos(), vector.add(
|
||||
dude2:get_pos(), vector.new(0, -5, -20))), 'boss not with dude2')
|
||||
mineunit:execute_on_leaveplayer(boss)
|
||||
mineunit:execute_globalstep(1)
|
||||
assert.equals(0, #chatlog, 'unexpected chatlog count')
|
||||
mineunit:execute_on_joinplayer(boss)
|
||||
mineunit:execute_globalstep(1)
|
||||
assert.is_false(vector.equals(boss:get_pos(),
|
||||
vector.add(dude2:get_pos(), vector.new(0, -5, -20))),
|
||||
'boss with dude2')
|
||||
end)
|
||||
end)
|
||||
|
7
spec/mineunit.conf
Normal file
7
spec/mineunit.conf
Normal file
@ -0,0 +1,7 @@
|
||||
fixture_paths = {
|
||||
"spec/fixtures",
|
||||
}
|
||||
exclude = {
|
||||
}
|
||||
verbose = 3
|
||||
|
Loading…
x
Reference in New Issue
Block a user