playerfactions/mtt.lua
Luke aka SwissalpS d6e7c62b84
Updated 5.9.0 (#12)
* Store hashes of passwords

cleartext password storage is bad practice.

* Depricate factions.get_password()

returns nil after first run

* loaded message

* Properly use the configurable admin priv in output

* Don't show password, since we can't anymore

* remove code that is never reached

* chown: reorder to first check if player has any factions at all

There is no point in checking other params if this part fails.

* chown: fix command signature

password is required

* proper admin priv listing in help for invite

* wrap fixup code in do-block

variable save_needed is not used for anything else

* locale: many -> multiple

* locale: remove unused entry

* locale: ownership rephrasing

* locale: tweak and add "No factions found."

* locale: exists -> exist

* locale: this -> that or better

also fixed a french mistake: player doesn't own these -> player owns
these

* locale: reuse string for missing name

besides, "nil" is a valid name. This way there is no confusion.

* locale: reuse "missing player name"

* locale: reuse "faction x doesn't exist"

* locale: faction x already exists

* locale: the player -> player x

* locale: some more de-Frenching

* add local is_admin

stash commit...

* disband: allow admin

- permit admin to disband a faction without having any factions himself
- permit admin to skip password check (he can supply any placeholder)
- permit admin to disband his own single faction
- don't call get_owner or valid_password if is admin
- streamline duplicate code

* list: check for true first instead of using negation

- check for no factions first -> simpler code
- whitespace: linebreak for easier reading

* info: cleanup

- whitespace linebreaks for easier reading and consistancy
- update helptext signiture (also for disband) to reflect actual
requirements and standard
- loop members into table for consistant and easier to read code

* player_info: cleanup

- move depricated log entry to start of get_player_faction(), no point
in skipping warning.
- simplify get_player_factions()
- whitespace linebreaks for easier reading and consistancy
- loop members into table for consistant and easier to read code
- simplify get_owned_factions()
- make player_name param optional, default to caller (still need to
check as caller name can be missing)
- loop factions into table for consistant and easier to read code (also
presumpted faster)

* join: cleanup

- don't call get_player_factions() unless needed
- use get_player_factions() instead of depricated get_player_faction()
- truth check of password in valid_password() for easier understanding
of code
- remove explicit nil check where not needed

* leave: cleanup

- update help text to standard syntax
- remove unnecessary param count checks
- simplify leave_faction() argument checking

* kick: cleanup

- simplify and reduce calls of core.get_player_privs()
- update help text to standard syntax
- streamline duplicate code
- remove unnecessary param count checks
- remove explicit nil check where not needed
- don't call get_owner if is admin (until needed)

* passwd: cleanup

- update help text to standard syntax
- streamline duplicate code
- remove unnecessary param count checks
- remove explicit nil check where not needed
- don't call get_owner if is admin

* chown: cleanup and tweak

- update help text to standard syntax
- streamline duplicate code
- remove unnecessary param count checks
- remove explicit nil check where not needed
- updated locale to be neutral to admin or owner
- don't call get_owner or valid_password if is admin
- remove core.player_exists() call since target was checked when joined
faction
- abort early if no target or password provided

* invite: cleanup and tweaks

- reduced needed indents
- remove explicit nil check where not needed
- use get_player_factions() instead of depricated get_player_faction()
and reduce calls of it
- tweaked join_faction()
- adds check if player already is in that faction

* more tweaks

- join: check if already member
- leave: checks if user is in given faction at all
- kick: early abort if no player provided
- create: early abort if no faction or password are provided
- create: use get_player_factions() instead of get_player_faction()
- create: reduce explicit nil checks
- disband: early abort if missing password
- disband: reduce param-count-checks and use table.getn()
- info: reduce explicit nil checks and use table.getn()
- passwd: early abort if no password provided
- in general remove explicit nil-checks where not needed

* is_admin -> not_admin

for slightly easier reading and shorter lines

* fix translator missing argument

* some facepalm fixes

and tweaks of table.getn() for consistency, here # would work just as
well.

* set minimum server version to 5.9.0

* another facepalm moment

* add mtt support

* refactor handle_command for mtt

It could've been done by only exposing handle_command, but this is
cleaner for future maintenance as tasks are well separated.

* bundle mtt related lines

* needs fakelib, not areas

areas will need this mod for testing

* remove unused arguments

* add owner to members on cleanup

* rename chat to cc

also no need to expose cc directly to mtt

* register the actually set priv when it is missing

* label data correctly

* move settings higher up where they are expected to be

* consistancy with variable names

use faction_name, player_name, target_name, password etc.
instead of a jumble of pw, fname, name, player_name etc.

* reduce needles table-copy

* fail to register same named factions

* no-op depricated and useless get_password

* some more checks in some API methods

* whitespace and comments

* pass translator to mtt

* bugfix cc.disband inverted password check

* standardize var name and reduce looping

* add get_members() api-method and use it

* player_info: count empty string as no player

* player_info: switch if-else to avoid negation

* unreachable comments

* simpler check

* add mtt-checks for front and backend commands

* update french locale

- informal tone
- adds missing entries

* add Spanish locale

* add German locale

* whitespace cleanup

* add fakelib comment

* provide alternative to table.pack()

* add disband hook support

* remove local f == factions
2024-12-31 19:45:10 +01:00

523 lines
19 KiB
Lua

-- requires [fakelib] to work properly
-- https://github.com/OgelGames/fakelib.git
local pd
if table.pack then
pd = function(...) print(dump(table.pack(...))) end
else
pd = function(...) for _, v in ipairs({ ... }) do print(dump(v)) end end
end
local fcc, S = factions.handle_command, factions.S
factions.mode_unique_faction = false
factions.max_members_list = 11
-- factions chat command checker
-- b1: expected return bool
-- s1: expected return string
-- n: name of command executor
-- c: string of command parameters (the "/factions " part cut off)
local function fccc(b1, s1, n, c)
local b2, s2 = fcc(n, c)
return b1 == b2 and s1 == s2
end
local function resetDB()
for k in pairs(factions.get_facts()) do
factions.disband_faction(k)
end
end
local function makeFactions()
return factions.register_faction('Endorian', 'Endor', 'eEe')
and factions.register_faction('Alberian', 'Albert', 'a')
and factions.register_faction('Gandalfian', 'Gandalf', 'GgG♥💩☺')
end
local function dbChecks(callback)
-- basic db integrity tests
local facts = factions.get_facts()
assert('table' == type(facts))
assert('table' == type(facts.Alberian))
assert('Albert' == facts.Alberian.owner)
assert('Alberian' == facts.Alberian.name)
assert('table' == type(facts.Alberian.members))
-- make sure owners have been added as memebers
assert(true == facts.Alberian.members.Albert)
-- hash tests, should never fail unless engine made a mistake
assert('8b2713b352c6fa2d22272a91612fba2f87d0c01885762a1522a7b4aec5592a80'
== facts.Endorian.password256)
assert('ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb'
== facts.Alberian.password256)
assert('3bfe911604e3fb079ad535a0c359a8457aea39d663bb4f21648842e3a4eaccf9'
== facts.Gandalfian.password256)
-- no more cleartext passwords (doesn't make sense in test-environement)
assert(nil == facts.Gandalfian.password)
callback()
end
mtt.register('reset db', function(callback) resetDB() callback() end)
mtt.register('join & setup players', function(callback)
-- some player
assert('table' == type(mtt.join_player('Endor')))
-- faction admin
assert('table' == type(mtt.join_player('Albert')))
-- some other player
assert('table' == type(mtt.join_player('Gandalf')))
-- player without privs or factions
assert('table' == type(mtt.join_player('HanSolo')))
-- make Albert a faction-admin
local player_privs = minetest.get_player_privs('Albert')
player_privs[factions.priv] = true
minetest.set_player_privs('Albert', player_privs)
-- make sure the others aren't
for _, name in ipairs({ 'Endor', 'Gandalf', 'HanSolo' }) do
player_privs = minetest.get_player_privs(name)
player_privs[factions.priv] = nil
minetest.set_player_privs(name, player_privs)
end
callback()
end)
mtt.register('some players leave', function(callback)
-- let's test without the admin online
assert(true == mtt.leave_player('Albert'))
assert(true == mtt.leave_player('Gandalf'))
callback()
end)
mtt.register('make factions with backend', function(callback)
assert(makeFactions())
callback()
end)
mtt.register('basic db checks', dbChecks)
mtt.register('backend functions: player_is_in_faction', function(callback)
assert(false == factions.player_is_in_faction(
'notExistingFaction', 'notExistingPlayer'))
assert(false == factions.player_is_in_faction(
'notExistingFaction', 'Gandalf'))
assert(false == factions.player_is_in_faction(
'Gandalfian', 'notExistingPlayer'))
assert(nil == factions.player_is_in_faction(
'Gandalfian', 'Albert'))
assert(true == factions.player_is_in_faction(
'Gandalfian', 'Gandalf'))
callback()
end)
mtt.register('backend functions: get_player_faction', function(callback)
-- (depricated) --> check log output for messages
assert(false == factions.get_player_faction('notExistingPlayer'))
assert(nil == factions.get_player_faction('HanSolo'))
assert('Alberian' == factions.get_player_faction('Albert'))
callback()
end)
mtt.register('backend functions: get_player_factions', function(callback)
if pcall(factions.get_player_factions, nil) then
callback('did not fail with nil as player argument -> bad')
end
if pcall(factions.get_player_factions, 42) then
callback('did not fail with number as player argument -> bad')
end
assert(false == factions.get_player_factions('notExistingPlayer'))
assert(false == factions.get_player_factions('HanSolo'))
assert('Alberian' == factions.get_player_factions('Albert')[1])
callback()
end)
mtt.register('backend functions: get_owned_factions', function(callback)
assert(false == factions.get_owned_factions(nil))
assert(false == factions.get_owned_factions(42))
assert(false == factions.get_owned_factions('notExistingPlayer'))
assert(false == factions.get_owned_factions('HanSolo'))
local t = factions.get_owned_factions('Albert')
assert(1 == #t and 'Alberian' == t[1])
callback()
end)
mtt.register('backend functions: get_administered_factions', function(callback)
if pcall(factions.get_administered_factions) then
callback('calling get_administered_factions with nil did not raise error')
end
-- a bit strange that number as player name 'works'
assert(false == factions.get_administered_factions(42))
assert(false == factions.get_administered_factions('notExistingPlayer'))
assert(false == factions.get_administered_factions('HanSolo'))
local t = factions.get_administered_factions('Gandalf')
assert(1 == #t and 'Gandalfian' == t[1])
assert(3 == #factions.get_administered_factions('Albert'))
callback()
end)
mtt.register('backend functions: get_owner', function(callback)
assert(false == factions.get_owner('notExistingFaction'))
assert('Gandalf' == factions.get_owner('Gandalfian'))
callback()
end)
mtt.register('backend functions: chown', function(callback)
assert(false == factions.chown('notExistingFaction', 'Gandalf'))
assert(true == factions.chown('Endorian', 'Gandalf'))
-- revert the 'illegal' use
factions.chown('Endorian', 'Endor')
callback()
end)
mtt.register('backend functions: register_faction', function(callback)
-- (partly tested in setup)
assert(false == factions.register_faction('Endorian', 'Endor', 'rodnE'))
assert(false == factions.register_faction())
-- empty password
assert(factions.register_faction('foo', 'bar', ''))
callback()
end)
mtt.register('backend functions: disband_faction', function(callback)
-- (partly tested in setup)
assert(factions.disband_faction('foo'))
assert(false == factions.disband_faction())
assert(false == factions.disband_faction('notExistingFaction'))
callback()
end)
mtt.register('backend functions: hash_password', function(callback)
-- (tested in basic db checks)
callback()
end)
mtt.register('backend functions: valid_password', function(callback)
assert(false == factions.valid_password())
assert(false == factions.valid_password('Endorian'))
assert(false == factions.valid_password('Endorian', 'foobar'))
assert(true == factions.valid_password('Endorian', 'eEe'))
callback()
end)
mtt.register('backend functions: get_password (depricated)', function(callback)
assert(nil == factions.get_password())
assert(nil == factions.get_password('Endorian'))
callback()
end)
mtt.register('backend functions: set_password', function(callback)
assert(false == factions.set_password('notExistingFaction', 'foobar'))
assert(false == factions.set_password('Endorian'))
assert(true == factions.set_password('Endorian', 'EeE'))
assert(factions.valid_password('Endorian', 'EeE'))
-- revert that again
factions.set_password('Endorian', 'eEe')
callback()
end)
mtt.register('backend functions: join_faction', function(callback)
assert(false == factions.join_faction())
assert(false == factions.join_faction('Endorian'))
assert(false == factions.join_faction('Endorian', 'notExistingPlayer'))
assert(true == factions.join_faction('Endorian', 'Gandalf'))
callback()
end)
mtt.register('backend functions: leave_faction', function(callback)
assert(false == factions.leave_faction())
assert(false == factions.leave_faction('Endorian'))
assert(false == factions.leave_faction('Endorian', 'notExistingPlayer'))
assert(true == factions.leave_faction('Endorian', 'Gandalf'))
callback()
end)
mtt.register('intermediate db checks', dbChecks)
mtt.register('frontend functions: no arguments', function(callback)
assert(fccc(false, S("Unknown subcommand. Run '/help factions' for help."),
'', ''))
callback()
end)
mtt.register('frontend functions: create', function(callback)
assert(fccc(false, S("Missing faction name."), 'Gandalf', 'create'))
assert(fccc(false, S("Missing password."), 'Gandalf', 'create foobar'))
assert(fccc(false, S("Faction @1 already exists.", 'Gandalfian'),
'Gandalf', 'create Gandalfian foobar'))
factions.mode_unique_faction = true
assert(fccc(false, S("You are already in a faction."),
'Gandalf', 'create Gandalfian2 foobar'))
factions.mode_unique_faction = false
-- correct creation (also with capitals in sub-command)
assert(fccc(true, S("Registered @1.", 'Gandalfian2'),
'Gandalf', 'cREate Gandalfian2 foobar'))
callback()
end)
mtt.register('frontend functions: disband', function(callback)
assert(fccc(false, S("Missing password."), 'Gandalf', 'disband'))
-- list order is not predictable, so we try both orders
assert(fccc(false, S(
"You are the owner of multiple factions, you have to choose one of them: @1.",
'Gandalfian, Gandalfian2'), 'Gandalf', 'disband foobar')
or fccc(false, S(
"You are the owner of multiple factions, you have to choose one of them: @1.",
'Gandalfian2, Gandalfian'), 'Gandalf', 'disband foobar'))
assert(fccc(false, S("You don't own any factions."), 'HanSolo',
'disband foobar'))
assert(fccc(false, S("Permission denied: You are not the owner of that faction,"
.. " and don't have the @1 privilege.", factions.priv),
'Endor', 'disband foobar Gandalfian2'))
assert(fccc(false, S("Permission denied: Wrong password."),
'Endor', 'disband foobar'))
assert(fccc(true, S("Disbanded @1.", 'Endorian'),
'Endor', 'disband eEe'))
-- admin disbands other player's faction w/o knowing password
assert(fccc(true, S("Disbanded @1.", 'Gandalfian2'),
'Albert', 'disband foobar Gandalfian2'))
assert(fccc(false, S("Faction @1 doesn't exist.", 'Gandalfian2'),
'Gandalf', 'disband eEe Gandalfian2'))
callback()
end)
mtt.register('frontend functions: list', function(callback)
assert(fccc(true, S("Factions (@1): @2.", 2, 'Gandalfian, Alberian'),
'', 'list')
or fccc(true, S("Factions (@1): @2.", 2, 'Alberian, Gandalfian'),
'', 'list'))
resetDB()
assert(fccc(true, S("There are no factions yet."), '', 'list'))
callback()
end)
mtt.register('frontend functions: info', function(callback)
assert(fccc(true, S("No factions found."), 'HanSolo', 'info'))
makeFactions()
assert(fccc(false, S("Faction @1 doesn't exist.", 'foobar'),
'Endor', 'info foobar'))
factions.join_faction('Endorian', 'Gandalf')
assert(fccc(false, S("You are in multiple factions, you have to choose one of them: @1.",
'Endorian, Gandalfian'), 'Gandalf', 'info')
or fccc(false, S("You are in multiple factions, you have to choose one of them: @1.",
'Gandalfian, Endorian'), 'Gandalf', 'info'))
-- SwissalpS can't be bothered to check some of these results in depth,
-- so just dumping result for optical check.
pd('Endor executes: /factions info', fcc('Endor', 'info'))
assert(fcc('Endor', 'info'))
factions.max_members_list = 1
pd('max_members_list == 1 and Endor executes: /factions info',
fcc('Endor', 'info'))
assert(fcc('Endor', 'info'))
factions.max_members_list = 11
pd('Endor executes: /factions info Gandalfian', fcc('Endor', 'info Gandalfian'))
assert(fcc('Endor', 'info Gandalfian'))
callback()
end)
mtt.register('frontend functions: player_info', function(callback)
-- should never happen
assert(fccc(false, S("Missing player name."), '', 'player_info'))
assert(fccc(false, S("Player @1 doesn't exist or isn't in any faction.",
'HanSolo'), 'HanSolo', 'player_info'))
assert(fccc(false, S("Player @1 doesn't exist or isn't in any faction.",
'notExistingPlayer'), 'Endor', 'player_info notExistingPlayer'))
assert(fccc(true, S("@1 is in the following factions: @2.",
'Endor', 'Endorian') .. "\n"
.. S("@1 is the owner of the following factions: @2.",
'Endor', 'Endorian'), 'Endor', 'player_info'))
assert(fccc(true, S("@1 is in the following factions: @2.",
'Albert', 'Alberian') .. "\n"
.. S("@1 is the owner of the following factions: @2.",
'Albert', 'Alberian') .. "\n"
.. S("@1 has the @2 privilege so they can admin every faction.",
'Albert', factions.priv), 'Endor', 'player_info Albert'))
callback()
end)
mtt.register('frontend functions: join', function(callback)
factions.mode_unique_faction = true
assert(fccc(false, S("You are already in a faction."),
'Endor', 'join'))
factions.mode_unique_faction = false
assert(fccc(false, S("Missing faction name."),
'Endor', 'join'))
assert(fccc(false, S("Faction @1 doesn't exist.", 'notExistingFaction'),
'Endor', 'join notExistingFaction'))
assert(fccc(false, S("You are already in faction @1.", 'Endorian'),
'Endor', 'join Endorian'))
assert(fccc(false, S("Permission denied: Wrong password."),
'Endor', 'join Gandalfian'))
assert(fccc(false, S("Permission denied: Wrong password."),
'Endor', 'join Gandalfian abc'))
assert(fccc(true, S("Joined @1.", 'Gandalfian'),
'Endor', 'join Gandalfian GgG♥💩☺'))
callback()
end)
mtt.register('frontend functions: leave', function(callback)
assert(fccc(false, S("You are not in a faction."),
'HanSolo', 'leave'))
assert(fccc(false, S("You are in multiple factions, you have to choose one of them: @1.",
'Gandalfian, Endorian'),
'Endor', 'leave')
or fccc(false, S("You are in multiple factions, you have to choose one of them: @1.",
'Endorian, Gandalfian'),
'Endor', 'leave'))
assert(fccc(false, S("Faction @1 doesn't exist.", 'notExistingFaction'),
'Endor', 'leave notExistingFaction'))
assert(fccc(false, S("You cannot leave your own faction, change owner or disband it."),
'Albert', 'leave'))
assert(fccc(false, S("You aren't part of faction @1.", 'Gandalfian'),
'Albert', 'leave Gandalfian'))
assert(fccc(true, S("Left @1.", 'Endorian'),
'Gandalf', 'leave Endorian'))
callback()
end)
mtt.register('frontend functions: kick', function(callback)
assert(fccc(false, S("Missing player name."),
'Gandalf', 'kick'))
assert(fccc(false, S("You don't own any factions, you can't use this command."),
'HanSolo', 'kick Endor'))
local b, s = fcc('Albert', 'kick Endor')
-- only works if run on English server
assert(false == b and nil ~= s:find('multiple factions,'))
assert(fccc(false, S("Permission denied: You are not the owner of that faction, "
.. "and don't have the @1 privilege.", factions.priv),
'Endor', 'kick Gandalf Gandalfian'))
assert(fccc(false, S("@1 is not in the specified faction.", 'Gandalf'),
'Endor', 'kick Gandalf Endorian'))
assert(fccc(false, S("You cannot kick the owner of a faction, "
.. "use '/factions chown <player> <password> [<faction>]' "
.. "to change the ownership."),
'Albert', 'kick Gandalf Gandalfian'))
assert(fccc(true, S("Kicked @1 from faction.", 'Endor'),
'Gandalf', 'kick Endor Gandalfian'))
callback()
end)
mtt.register('frontend functions: passwd', function(callback)
assert(fccc(false, S("Missing password."),
'HanSolo', 'passwd'))
assert(fccc(false, S("You don't own any factions, you can't use this command."),
'HanSolo', 'passwd foobar'))
local b, s = fcc('Albert', 'passwd foobar')
-- only works on English locale
assert(false == b and nil ~= s:find('multiple factions'))
assert(fccc(false, S("Permission denied: You are not the owner of that faction, "
.. "and don't have the @1 privilege.", factions.priv),
'Endor', 'passwd foobar Gandalfian'))
assert(fccc(true, S("Password has been updated."),
'Endor', 'passwd foobar'))
assert(factions.get_facts().Endorian.password256 ==
'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2')
assert(fccc(true, S("Password has been updated."),
'Gandalf', 'passwd foobar Gandalfian'))
assert(factions.get_facts().Gandalfian.password256 ==
'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2')
assert(fccc(true, S("Password has been updated."),
'Albert', 'passwd barf Gandalfian'))
assert(factions.get_facts().Gandalfian.password256 ==
'8a6e40cfcd99060eb1efdfeb689fe26606e221b4fd487bb224ab79a82648ccd9')
callback()
end)
mtt.register('frontend functions: chown', function(callback)
assert(fccc(false, S("Missing player name."),
'Gandalf', 'chown'))
assert(fccc(false, S("Missing password."),
'Gandalf', 'chown notExistingPlayer'))
assert(fccc(false, S("You don't own any factions, you can't use this command."),
'HanSolo', 'chown notExistingPlayer foobar'))
local b, s = fcc('Albert', 'chown notExistingPlayer foobar')
assert(false == b and nil ~= s:find('multiple factions'))
assert(fccc(false, S("Permission denied: You are not the owner of that faction, "
.. "and don't have the @1 privilege.", factions.priv),
'Gandalf', 'chown Endor foobar Endorian'))
assert(fccc(false, S("@1 isn't in faction @2.", 'notExistingPlayer', 'Gandalfian'),
'Gandalf', 'chown notExistingPlayer foobar'))
assert(fccc(false, S("@1 isn't in faction @2.", 'Endor', 'Gandalfian'),
'Gandalf', 'chown Endor foobar'))
factions.join_faction('Gandalfian', 'Endor')
assert(fccc(false, S("Permission denied: Wrong password."),
'Gandalf', 'chown Endor foobar'))
assert(fccc(true, S("Ownership has been transferred to @1.", 'Endor'),
'Gandalf', 'chown Endor barf'))
assert('Endor' == factions.get_owner('Gandalfian'))
assert(fccc(true, S("Ownership has been transferred to @1.", 'Gandalf'),
'Albert', 'chown Gandalf foobar Gandalfian'))
assert('Gandalf' == factions.get_owner('Gandalfian'))
callback()
end)
mtt.register('frontend functions: invite', function(callback)
assert(fccc(false, S("Permission denied: You can't use this command, @1 priv is needed.",
factions.priv), 'notExistingPlayer', 'invite'))
assert(fccc(false, S("Permission denied: You can't use this command, @1 priv is needed.",
factions.priv), 'Endor', 'invite'))
assert(fccc(false, S("Missing player name."), 'Albert', 'invite'))
assert(fccc(false, S("Missing faction name."), 'Albert', 'invite Endor'))
assert(fccc(false, S("Faction @1 doesn't exist.", 'notExistingFaction'),
'Albert', 'invite Endor notExistingFaction'))
assert(fccc(false, S("Player @1 doesn't exist.", 'notExistingPlayer'),
'Albert', 'invite notExistingPlayer Gandalfian'))
assert(fccc(false, S("Player @1 is already in faction @2.", 'Endor', 'Gandalfian'),
'Albert', 'invite Endor Gandalfian'))
factions.mode_unique_faction = true
assert(fccc(false, S("Player @1 is already in faction @2.", 'Gandalf', 'Gandalfian'),
'Albert', 'invite Gandalf Endorian'))
factions.mode_unique_faction = false
assert(fccc(true, S("@1 is now a member of faction @2.", 'Gandalf', 'Endorian'),
'Albert', 'invite Gandalf Endorian'))
callback()
end)
mtt.register('final db checks', function(callback)
pd(factions.get_facts())
callback()
end)
mtt.register('remaining players leave', function(callback)
assert(true == mtt.leave_player('Endor'))
assert(true == mtt.leave_player('HanSolo'))
callback()
end)
mtt.register('final', function(callback)
print('total success')
callback()
end)