mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53: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 {
|
||||
|
||||
Reference in New Issue
Block a user