feat(game): slash commands accept permission grants

A command may be used when the character's GM level allows it or a grant does: a grant of the command, of every
command up to a GM level, or of the dashboard permission the command follows (so accounts_kick covers /kick). A deny
of any of those takes it away even when the level allows it, except from GM 9 accounts (also while they play at a
lower level). Grants never take anyone below a command's floor above GM 1 (/execute), and commands the client handles
keep their fixed level. Expired grants count for nothing. The grants are the account's and the logged-in
character's, read from the database the first time a command is used and kept on the User. /help lists the commands
a player may use this way. The self and rank rules for commands that act on another player count grants of self_* and
manage_equal_rank too. A denied command says it was taken away.

Check: grant a GM 0 account /spawn (or "every command up to GM 8") on the dashboard, relog, and /spawn works and
shows in /help; deny /spawn from a GM 8 character: "it was taken away"; grant accounts_kick and /kick works (the rank
rules still apply to whom). dGameTests SlashCommandGrantsTest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 01:11:17 -05:00
parent 868f34123f
commit e00d22152e
4 changed files with 186 additions and 10 deletions

View File

@@ -267,3 +267,117 @@ TEST_F(SlashCommandCatalogTest, NewServerFollowsThePermissions) {
Game::config->SetDatabaseValues({}, {});
EXPECT_EQ(static_cast<uint8_t>(SlashCommandHandler::GetRequiredLevel(*SlashCommandHandler::FindCommand("mute"))), Permissions::Level("accounts_mute"));
}
#include "Character.h"
#include "Entity.h"
#include "PermissionGrants.h"
#include "User.h"
// Grants and denies from the dashboard (PermissionGrants.h), as a player using commands sees them
class SlashCommandGrantsTest : public GameDependenciesTest {
protected:
static constexpr LWOOBJID CHARACTER_ID = 42;
void SetUp() override {
SetUpDependencies();
static bool started = false;
if (!started) SlashCommandHandler::Startup();
started = true;
user = std::make_unique<User>(UNASSIGNED_SYSTEM_ADDRESS, "tester", "key");
character = std::make_unique<Character>(CHARACTER_ID, user.get());
entity = std::make_unique<Entity>(1, info);
entity->SetCharacter(character.get());
character->SetEntity(entity.get());
}
void TearDown() override {
entity->SetCharacter(nullptr);
entity.reset();
character.reset();
user.reset();
TearDownDependencies();
}
// Play at a GM level (the account's is at least that) with these grants loaded for the character
void Play(eGameMasterLevel level, std::vector<PermissionGrants::Rule> rules, eGameMasterLevel accountLevel = eGameMasterLevel::CIVILIAN) {
user->SetMaxGMLevel(std::max(level, accountLevel));
entity->SetGMLevel(level);
auto held = std::make_shared<PermissionGrants::Held>();
held->rules = std::move(rules);
user->SetGrants(held, CHARACTER_ID);
}
bool MayUse(const std::string& alias) {
const auto* command = SlashCommandHandler::FindCommand(alias);
EXPECT_NE(command, nullptr) << alias;
return command && SlashCommandHandler::MayUse(entity.get(), *command);
}
std::unique_ptr<User> user;
std::unique_ptr<Character> character;
std::unique_ptr<Entity> entity;
};
TEST_F(SlashCommandGrantsTest, GrantsLetAPlayerUseACommand) {
using PermissionGrants::eKind;
Play(eGameMasterLevel::MODERATOR, {});
EXPECT_FALSE(MayUse("spawn"));
EXPECT_TRUE(MayUse("kick")); // accounts_kick: GM 2
Play(eGameMasterLevel::MODERATOR, { { eKind::COMMAND, "spawn", false, 0 } });
EXPECT_TRUE(MayUse("spawn"));
EXPECT_FALSE(MayUse("gmadditem"));
// A grant of a dashboard permission covers the commands that follow it
Play(eGameMasterLevel::CIVILIAN, { { eKind::PERMISSION, "accounts_kick", false, 0 } });
EXPECT_TRUE(MayUse("kick"));
// Every command up to a GM level
Play(eGameMasterLevel::CIVILIAN, { { eKind::COMMAND_GROUP, "8", false, 0 } });
EXPECT_TRUE(MayUse("spawn"));
EXPECT_TRUE(MayUse("gmadditem"));
// ...but never /execute below its floor
Play(eGameMasterLevel::MODERATOR, { { eKind::COMMAND_GROUP, "9", false, 0 }, { eKind::COMMAND, "execute", false, 0 } });
EXPECT_FALSE(MayUse("execute"));
// Past its expiry a grant does nothing
Play(eGameMasterLevel::MODERATOR, { { eKind::COMMAND, "spawn", false, 1 } });
EXPECT_FALSE(MayUse("spawn"));
}
TEST_F(SlashCommandGrantsTest, DeniesTakeCommandsAwayExceptFromOperators) {
using PermissionGrants::eKind;
Play(eGameMasterLevel::DEVELOPER, { { eKind::COMMAND, "spawn", true, 0 }, { eKind::PERMISSION, "accounts_kick", true, 0 } });
EXPECT_FALSE(MayUse("spawn"));
EXPECT_FALSE(MayUse("kick"));
EXPECT_TRUE(MayUse("gmadditem"));
// A GM 9 account playing at GM 8 keeps everything
Play(eGameMasterLevel::DEVELOPER, { { eKind::COMMAND, "spawn", true, 0 } }, eGameMasterLevel::OPERATOR);
EXPECT_TRUE(MayUse("spawn"));
// A player command can be taken from a player
Play(eGameMasterLevel::CIVILIAN, { { eKind::COMMAND, "pvp", true, 0 } });
EXPECT_FALSE(MayUse("pvp"));
}
TEST_F(SlashCommandGrantsTest, ForgottenGrantsAreReadAgain) {
using PermissionGrants::eKind;
Play(eGameMasterLevel::MODERATOR, { { eKind::COMMAND, "spawn", false, 0 } });
EXPECT_TRUE(MayUse("spawn"));
// The dashboard changed them: read from the database again (none there)
user->ForgetGrants();
EXPECT_FALSE(MayUse("spawn"));
ASSERT_NE(user->GetGrants(), nullptr);
EXPECT_EQ(user->GetGrantsCharacter(), CHARACTER_ID);
}
TEST(SlashCommandPermissionTests, TargetRulesFollowGrants) {
using SlashCommandLevels::eTargetRule;
using AccountRules::eManageDenial;
using PermissionGrants::eKind;
ScopedConfig config({});
PermissionGrants::Held held;
held.rules = { { eKind::PERMISSION, "self_moderation", false, 0 }, { eKind::PERMISSION, "manage_equal_rank", false, 0 } };
EXPECT_EQ(SlashCommandHandler::TargetDenial(8, 1, 8, 1, eTargetRule::MODERATION), eManageDenial::SELF);
EXPECT_EQ(SlashCommandHandler::TargetDenial(8, 1, 8, 1, eTargetRule::MODERATION, &held), eManageDenial::NONE);
EXPECT_EQ(SlashCommandHandler::TargetDenial(4, 1, 4, 2, eTargetRule::MODERATION, &held), eManageDenial::NONE);
// Never a higher GM level
EXPECT_EQ(SlashCommandHandler::TargetDenial(4, 1, 5, 2, eTargetRule::MODERATION, &held), eManageDenial::HIGHER_RANK);
}