mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
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:
11
dGame/User.h
11
dGame/User.h
@@ -8,10 +8,12 @@
|
||||
#include "dCommonVars.h"
|
||||
#include "eFunnessTypes.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
class Character;
|
||||
enum class eGameMasterLevel : uint8_t;
|
||||
namespace PermissionGrants { struct Held; }
|
||||
|
||||
struct BehaviorParams {
|
||||
uint32_t behavior;
|
||||
@@ -59,6 +61,13 @@ public:
|
||||
|
||||
void UserOutOfSync(const CaughtFunness& funness);
|
||||
|
||||
// The permission grants of this account and of the character they were loaded for (SlashCommandHandler loads them
|
||||
// when first needed; nullptr until then, and again after ForgetGrants when the dashboard changed them)
|
||||
const std::shared_ptr<const PermissionGrants::Held>& GetGrants() const { return m_Grants; }
|
||||
LWOOBJID GetGrantsCharacter() const { return m_GrantsCharacter; }
|
||||
void SetGrants(std::shared_ptr<const PermissionGrants::Held> grants, LWOOBJID characterId) { m_Grants = std::move(grants); m_GrantsCharacter = characterId; }
|
||||
void ForgetGrants() { m_Grants.reset(); }
|
||||
|
||||
private:
|
||||
uint32_t m_AccountID;
|
||||
std::string m_Username;
|
||||
@@ -78,6 +87,8 @@ private:
|
||||
uint64_t m_MuteExpire;
|
||||
std::chrono::steady_clock::time_point m_LastMuteCheck{};
|
||||
std::vector<CaughtFunness> m_CaughtFunness{};
|
||||
std::shared_ptr<const PermissionGrants::Held> m_Grants{};
|
||||
LWOOBJID m_GrantsCharacter{};
|
||||
};
|
||||
|
||||
#endif // USER_H
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "ChatPackets.h"
|
||||
#include "WorldMigration.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <ranges>
|
||||
#include <set>
|
||||
@@ -26,6 +28,8 @@
|
||||
#include "dConfig.h"
|
||||
#include "SlashCommandLevels.h"
|
||||
#include "Permissions.h"
|
||||
#include "PermissionGrants.h"
|
||||
#include "PermissionGrantsLoader.h"
|
||||
#include "Character.h"
|
||||
#include "ChatPackets.h"
|
||||
#include "PlayerManager.h"
|
||||
@@ -123,19 +127,47 @@ std::optional<CommandTarget> SlashCommandHandler::FindTarget(const std::string&
|
||||
return target;
|
||||
}
|
||||
|
||||
AccountRules::eManageDenial SlashCommandHandler::TargetDenial(uint8_t actorLevel, uint32_t actorAccountId, uint8_t targetLevel, uint32_t targetAccountId, SlashCommandLevels::eTargetRule rule) {
|
||||
AccountRules::eManageDenial SlashCommandHandler::TargetDenial(uint8_t actorLevel, uint32_t actorAccountId, uint8_t targetLevel, uint32_t targetAccountId, SlashCommandLevels::eTargetRule rule,
|
||||
const PermissionGrants::Held* actorGrants) {
|
||||
using SlashCommandLevels::eTargetRule;
|
||||
using AccountRules::eAccountAction;
|
||||
const auto denial = [&](eAccountAction action) { return AccountRules::ManageDenialNow(actorLevel, actorAccountId, targetLevel, targetAccountId, action, nullptr, actorGrants); };
|
||||
switch (rule) {
|
||||
case eTargetRule::NONE: return AccountRules::eManageDenial::NONE;
|
||||
case eTargetRule::OTHERS:
|
||||
if (actorAccountId != 0 && actorAccountId == targetAccountId) return AccountRules::eManageDenial::NONE;
|
||||
return AccountRules::ManageDenialNow(actorLevel, actorAccountId, targetLevel, targetAccountId, AccountRules::eAccountAction::TOOLS);
|
||||
case eTargetRule::ITEMS: return AccountRules::ManageDenialNow(actorLevel, actorAccountId, targetLevel, targetAccountId, AccountRules::eAccountAction::ITEMS);
|
||||
case eTargetRule::MODERATION: return AccountRules::ManageDenialNow(actorLevel, actorAccountId, targetLevel, targetAccountId, AccountRules::eAccountAction::MODERATION);
|
||||
default: return AccountRules::ManageDenialNow(actorLevel, actorAccountId, targetLevel, targetAccountId, AccountRules::eAccountAction::TOOLS);
|
||||
return denial(eAccountAction::TOOLS);
|
||||
case eTargetRule::ITEMS: return denial(eAccountAction::ITEMS);
|
||||
case eTargetRule::MODERATION: return denial(eAccountAction::MODERATION);
|
||||
default: return denial(eAccountAction::TOOLS);
|
||||
}
|
||||
}
|
||||
|
||||
const PermissionGrants::Held* SlashCommandHandler::GrantsOf(Entity* player) {
|
||||
auto* character = player ? player->GetCharacter() : nullptr;
|
||||
auto* user = character ? character->GetParentUser() : nullptr;
|
||||
if (!user) return nullptr;
|
||||
if (!user->GetGrants() || user->GetGrantsCharacter() != character->GetID()) {
|
||||
user->SetGrants(PermissionGrants::Load(user->GetAccountID(), character->GetID()), character->GetID());
|
||||
}
|
||||
return user->GetGrants().get();
|
||||
}
|
||||
|
||||
PermissionGrants::Command SlashCommandHandler::GrantRules(const Command& command) {
|
||||
return { command.name, static_cast<uint8_t>(GetRequiredLevel(command)), static_cast<uint8_t>(command.minLevel.value_or(eGameMasterLevel::CIVILIAN)),
|
||||
command.fixedLevel, Permissions::Find(command.dashboardPermission) ? command.dashboardPermission : "" };
|
||||
}
|
||||
|
||||
bool SlashCommandHandler::MayUse(Entity* player, const Command& command) {
|
||||
if (!player) return false;
|
||||
auto* character = player->GetCharacter();
|
||||
auto* user = character ? character->GetParentUser() : nullptr;
|
||||
const auto playerLevel = static_cast<uint8_t>(player->GetGMLevel());
|
||||
// Denies never apply to a GM 9 account, even while it plays at a lower level
|
||||
const auto accountLevel = std::max(playerLevel, static_cast<uint8_t>(user ? user->GetMaxGMLevel() : eGameMasterLevel::CIVILIAN));
|
||||
return PermissionGrants::MayUseCommand(playerLevel, accountLevel, GrantRules(command), GrantsOf(player), static_cast<int64_t>(std::time(nullptr)));
|
||||
}
|
||||
|
||||
std::string SlashCommandHandler::TargetRefusal(AccountRules::eManageDenial denial, SlashCommandLevels::eTargetRule rule, std::string_view command) {
|
||||
if (denial == AccountRules::eManageDenial::NONE) return "";
|
||||
using SlashCommandLevels::eTargetRule;
|
||||
@@ -146,7 +178,7 @@ std::string SlashCommandHandler::TargetRefusal(AccountRules::eManageDenial denia
|
||||
|
||||
bool SlashCommandHandler::MayActOn(Entity* actor, const SystemAddress& sysAddr, const CommandTarget& target, SlashCommandLevels::eTargetRule rule, std::string_view command) {
|
||||
if (!actor) return false;
|
||||
const auto denial = TargetDenial(static_cast<uint8_t>(actor->GetGMLevel()), AccountOf(actor), target.gmLevel, target.accountId, rule);
|
||||
const auto denial = TargetDenial(static_cast<uint8_t>(actor->GetGMLevel()), AccountOf(actor), target.gmLevel, target.accountId, rule, GrantsOf(actor));
|
||||
if (denial == AccountRules::eManageDenial::NONE) return true;
|
||||
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::UTF8ToUTF16(TargetRefusal(denial, rule, command)));
|
||||
return false;
|
||||
@@ -249,9 +281,12 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& chat, Entity*
|
||||
if (commandItr != RegisteredCommands.end()) {
|
||||
auto& [alias, commandHandle] = *commandItr;
|
||||
const auto requiredLevel = GetRequiredLevel(commandHandle);
|
||||
if (entity->GetGMLevel() >= requiredLevel) {
|
||||
if (MayUse(entity, commandHandle)) {
|
||||
if (requiredLevel > eGameMasterLevel::CIVILIAN) Database::Get()->InsertSlashCommandUsage(entity->GetObjectID(), input);
|
||||
commandHandle.handle(entity, sysAddr, args);
|
||||
} else if (entity->GetGMLevel() >= requiredLevel) {
|
||||
// The level allows it, but a deny on the dashboard took it away
|
||||
error = "You may not use \"" + command + "\": it was taken away from you";
|
||||
} else if (entity->GetGMLevel() != eGameMasterLevel::CIVILIAN) {
|
||||
error = "You are not high enough GM level to use \"" + command + "\"";
|
||||
}
|
||||
@@ -282,7 +317,7 @@ void GMZeroCommands::Help(Entity* entity, const SystemAddress& sysAddr, const st
|
||||
|
||||
std::map<std::string, Command> accessibleCommands;
|
||||
for (const auto& [commandName, command] : CommandInfos) {
|
||||
if (SlashCommandHandler::GetRequiredLevel(command) <= entity->GetGMLevel()) {
|
||||
if (SlashCommandHandler::MayUse(entity, command)) {
|
||||
accessibleCommands.emplace(commandName, command);
|
||||
}
|
||||
}
|
||||
@@ -312,7 +347,7 @@ void GMZeroCommands::Help(Entity* entity, const SystemAddress& sysAddr, const st
|
||||
}
|
||||
|
||||
const auto it = RegisteredCommands.find(trimmedArgs);
|
||||
if (it != RegisteredCommands.end() && entity->GetGMLevel() >= SlashCommandHandler::GetRequiredLevel(it->second)) {
|
||||
if (it != RegisteredCommands.end() && SlashCommandHandler::MayUse(entity, it->second)) {
|
||||
const auto& command = it->second;
|
||||
feedback << "----- " << it->first << " Info -----\n";
|
||||
feedback << command.info << "\n";
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "dCommonVars.h"
|
||||
#include "AccountRules.h"
|
||||
#include "SlashCommandLevels.h"
|
||||
#include "PermissionGrants.h"
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -80,7 +81,8 @@ namespace SlashCommandHandler {
|
||||
* their own level only with manage_equal_rank, and on themselves only with the self_* permission for the rule
|
||||
* (OTHERS: on themselves always, as before).
|
||||
*/
|
||||
AccountRules::eManageDenial TargetDenial(uint8_t actorLevel, uint32_t actorAccountId, uint8_t targetLevel, uint32_t targetAccountId, SlashCommandLevels::eTargetRule rule);
|
||||
AccountRules::eManageDenial TargetDenial(uint8_t actorLevel, uint32_t actorAccountId, uint8_t targetLevel, uint32_t targetAccountId, SlashCommandLevels::eTargetRule rule,
|
||||
const PermissionGrants::Held* actorGrants = nullptr);
|
||||
|
||||
// Why a command may not be used on someone, for the chat; empty when it may
|
||||
std::string TargetRefusal(AccountRules::eManageDenial denial, SlashCommandLevels::eTargetRule rule, std::string_view command);
|
||||
@@ -93,6 +95,20 @@ namespace SlashCommandHandler {
|
||||
|
||||
// The account of the player using a command (0 if unknown)
|
||||
uint32_t AccountOf(Entity* player);
|
||||
|
||||
/**
|
||||
* The permission grants of a player's account and logged-in character (PermissionGrants.h), loaded from the database
|
||||
* the first time they are needed and kept on the User until the dashboard changes them (DashboardActions). nullptr:
|
||||
* not a player.
|
||||
*/
|
||||
const PermissionGrants::Held* GrantsOf(Entity* player);
|
||||
|
||||
// Whether a player may use a command now: its level, or a grant for it (the command, a command group, or the
|
||||
// dashboard permission it follows), unless a deny takes it away (PermissionGrants::MayUseCommand)
|
||||
bool MayUse(Entity* player, const Command& command);
|
||||
|
||||
// The command as the grant rules see it: its name, level now, floor, and the dashboard permission it follows
|
||||
PermissionGrants::Command GrantRules(const Command& command);
|
||||
};
|
||||
|
||||
namespace GMZeroCommands {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user