## Feature Flag System A runtime feature flag system for dynamically enabling/disabling game systems and blocking specific game elements without server restarts. ### Overview Two types of controls: 1. Feature Flags - Named boolean flags that gate entire systems (trading, PvP, vendors, housing, etc.) 2. Dynamic Blocks - Block specific gumps, items, skills, or spells by type All changes are persisted to JSON, broadcast to online staff, and bypass-able by administrators. ### Predefined Flags ``` ┌─────────────────┬──────────┬─────────────────────────────────────┐ │ Flag │ Category │ Description │ ├─────────────────┼──────────┼─────────────────────────────────────┤ │ player_trading │ Economy │ Allow secure trades between players │ ├─────────────────┼──────────┼─────────────────────────────────────┤ │ vendor_purchase │ Economy │ Allow purchasing from NPC vendors │ ├─────────────────┼──────────┼─────────────────────────────────────┤ │ vendor_sell │ Economy │ Allow selling to NPC vendors │ ├─────────────────┼──────────┼─────────────────────────────────────┤ │ player_vendors │ Economy │ Allow player vendor interactions │ ├─────────────────┼──────────┼─────────────────────────────────────┤ │ bank_access │ Economy │ Allow bank box access │ ├─────────────────┼──────────┼─────────────────────────────────────┤ │ house_placement │ Housing │ Allow new house placements │ ├─────────────────┼──────────┼─────────────────────────────────────┤ │ boat_placement │ Housing │ Allow new boat placements │ ├─────────────────┼──────────┼─────────────────────────────────────┤ │ bulk_orders │ Crafting │ Allow bulk order deeds │ ├─────────────────┼──────────┼─────────────────────────────────────┤ │ pvp_combat │ Combat │ Allow player vs player combat │ └─────────────────┴──────────┴─────────────────────────────────────┘ ``` ### Commands #### Feature Flags ``` ┌────────────────────────────────────────────────────┬───────────────────────┐ │ Command │ Description │ ├────────────────────────────────────────────────────┼───────────────────────┤ │ [FeatureFlag <key> on|off|toggle|info │ Manage a feature flag │ ├────────────────────────────────────────────────────┼───────────────────────┤ │ [FF <key> on|off │ Shorthand alias │ ├────────────────────────────────────────────────────┼───────────────────────┤ │ [FeatureFlag <key> create <category> <desc> │ Create a custom flag │ ├────────────────────────────────────────────────────┼───────────────────────┤ │ [FeatureFlag <key> delete │ Delete a custom flag │ ├────────────────────────────────────────────────────┼───────────────────────┤ │ [FeatureList [flags|gumps|items|skills|spells|all] │ List all flags/blocks │ ├────────────────────────────────────────────────────┼───────────────────────┤ │ [FeatureAdmin │ Open the admin gump │ └────────────────────────────────────────────────────┴───────────────────────┘ ``` #### Gump Blocks ``` ┌────────────────────────────────┬──────────────────────────────────────────────────────┐ │ Command │ Description │ ├────────────────────────────────┼──────────────────────────────────────────────────────┤ │ [BlockGump <typeName> [reason] │ Block a gump type from displaying │ ├────────────────────────────────┼──────────────────────────────────────────────────────┤ │ [UnblockGump <typeName> │ Remove a gump block │ ├────────────────────────────────┼──────────────────────────────────────────────────────┤ │ [ListGumps [self] │ List open gumps on a player (for finding type names) │ └────────────────────────────────┴──────────────────────────────────────────────────────┘ ``` #### Item Blocks ``` ┌────────────────────────────────────────────────┬───────────────────────────────┐ │ Command │ Description │ ├────────────────────────────────────────────────┼───────────────────────────────┤ │ [BlockItemUse <typeName|target> [reason] │ Block item use │ ├────────────────────────────────────────────────┼───────────────────────────────┤ │ [BlockItemEquip <typeName|target> [reason] │ Block item equipping │ ├────────────────────────────────────────────────┼───────────────────────────────┤ │ [BlockItemContainer <typeName|target> [reason] │ Block container access │ ├────────────────────────────────────────────────┼───────────────────────────────┤ │ [UnblockItemUse <typeName> │ Remove use block │ ├────────────────────────────────────────────────┼───────────────────────────────┤ │ [UnblockItemEquip <typeName> │ Remove equip block │ ├────────────────────────────────────────────────┼───────────────────────────────┤ │ [UnblockItemContainer <typeName> │ Remove container access block │ └────────────────────────────────────────────────┴───────────────────────────────┘ ``` Item block commands create an entry if one doesn't exist, or flip the relevant flag on an existing entry. Unblock commands only clear their specific flag -- the entry is removed when all flags (use/equip/container) are off. #### Skill & Spell Blocks ``` ┌──────────────────────────────────────┬──────────────────────┐ │ Command │ Description │ ├──────────────────────────────────────┼──────────────────────┤ │ [BlockSkill <SkillName> [reason] │ Block a skill │ ├──────────────────────────────────────┼──────────────────────┤ │ [UnblockSkill <SkillName> │ Remove a skill block │ ├──────────────────────────────────────┼──────────────────────┤ │ [BlockSpell <SpellTypeName> [reason] │ Block a spell │ ├──────────────────────────────────────┼──────────────────────┤ │ [UnblockSpell <SpellTypeName> │ Remove a spell block │ └──────────────────────────────────────┴──────────────────────┘ ``` ### Architecture - Static bool classes for hot-path checks (no dictionary lookups): ServerFeatureFlags (Server project) and ContentFeatureFlags (UOContent) - Synced automatically by FeatureFlagManager.SyncStaticFlag() when flags change - Predefined flags loaded from Distribution/Configuration/FeatureFlags/default-flags.json - Runtime state persisted to Configuration/FeatureFlags/*.json (flags, gump-blocks, item-blocks, skill-blocks, spell-blocks) - Admin gump with tabbed UI for managing all block types, per-entry PropertiesGump editing, and pagination - All staff at AccessLevel.GameMaster+ bypass dynamic blocks; AccessLevel.Administrator+ bypass feature flags ### Hook Points Hook: Player trading Location: Mobile.OpenTrade Mechanism: ServerFeatureFlags.PlayerTrading ──────────────────────────────────────── Hook: PvP combat Location: Mobile.CanBeHarmful Mechanism: ServerFeatureFlags.PvPCombat ──────────────────────────────────────── Hook: Bank access Location: BankBox.Open() Mechanism: ServerFeatureFlags.BankAccess ──────────────────────────────────────── Hook: Vendor buy/sell Location: BaseVendor Mechanism: ContentFeatureFlags.VendorPurchase/Sell ──────────────────────────────────────── Hook: Player vendors Location: PlayerVendor Mechanism: ContentFeatureFlags.PlayerVendors ──────────────────────────────────────── Hook: House placement Location: HousePlacement.Check() Mechanism: ContentFeatureFlags.HousePlacement (returns BadRegionTemp) ──────────────────────────────────────── Hook: Boat placement Location: BaseBoatDeed.OnDoubleClick/OnPlacement Mechanism: ContentFeatureFlags.BoatPlacement ──────────────────────────────────────── Hook: Bulk orders Location: SmallBOD/LargeBOD Mechanism: ContentFeatureFlags.BulkOrders ──────────────────────────────────────── Hook: Gump display Location: GumpSystem.SendGump Mechanism: FeatureFlagManager.IsGumpBlocked ──────────────────────────────────────── Hook: Item use Location: PlayerMobile.AllowItemUse Mechanism: FeatureFlagManager.IsItemUseBlocked ──────────────────────────────────────── Hook: Item equip Location: PlayerMobile.CheckEquip Mechanism: FeatureFlagManager.IsItemEquipBlocked ──────────────────────────────────────── Hook: Container access Location: BaseContainer.DisplayTo Mechanism: FeatureFlagManager.IsContainerAccessBlocked ──────────────────────────────────────── Hook: Skill use Location: PlayerMobile.AllowSkillUse Mechanism: FeatureFlagManager.IsSkillBlocked ──────────────────────────────────────── Hook: Spell casting Location: Spell.Cast / Spellbook.CastSpellRequest Mechanism: FeatureFlagManager.IsSpellBlocked
602 lines
22 KiB
C#
602 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Server.Gumps;
|
|
using Server.Targeting;
|
|
|
|
namespace Server.Systems.FeatureFlags;
|
|
|
|
public static class FeatureFlagCommands
|
|
{
|
|
public static void Configure()
|
|
{
|
|
CommandSystem.Register("FeatureFlag", AccessLevel.Administrator, FeatureFlag_OnCommand);
|
|
CommandSystem.Register("FF", AccessLevel.Administrator, FeatureFlag_OnCommand);
|
|
CommandSystem.Register("BlockGump", AccessLevel.Administrator, BlockGump_OnCommand);
|
|
CommandSystem.Register("UnblockGump", AccessLevel.Administrator, UnblockGump_OnCommand);
|
|
CommandSystem.Register("BlockItemUse", AccessLevel.Administrator, BlockItemUse_OnCommand);
|
|
CommandSystem.Register("BlockItemEquip", AccessLevel.Administrator, BlockItemEquip_OnCommand);
|
|
CommandSystem.Register("BlockItemContainer", AccessLevel.Administrator, BlockItemContainer_OnCommand);
|
|
CommandSystem.Register("UnblockItemUse", AccessLevel.Administrator, UnblockItemUse_OnCommand);
|
|
CommandSystem.Register("UnblockItemEquip", AccessLevel.Administrator, UnblockItemEquip_OnCommand);
|
|
CommandSystem.Register("UnblockItemContainer", AccessLevel.Administrator, UnblockItemContainer_OnCommand);
|
|
CommandSystem.Register("BlockSkill", AccessLevel.Administrator, BlockSkill_OnCommand);
|
|
CommandSystem.Register("UnblockSkill", AccessLevel.Administrator, UnblockSkill_OnCommand);
|
|
CommandSystem.Register("BlockSpell", AccessLevel.Administrator, BlockSpell_OnCommand);
|
|
CommandSystem.Register("UnblockSpell", AccessLevel.Administrator, UnblockSpell_OnCommand);
|
|
CommandSystem.Register("FeatureList", AccessLevel.GameMaster, FeatureList_OnCommand);
|
|
CommandSystem.Register("FeatureAdmin", AccessLevel.Administrator, FeatureAdmin_OnCommand);
|
|
CommandSystem.Register("ListGumps", AccessLevel.GameMaster, ListGumps_OnCommand);
|
|
}
|
|
|
|
[Usage("FeatureFlag <flagKey> [on|off|toggle|info|create|delete]")]
|
|
[Aliases("FF")]
|
|
[Description("Manage feature flags. Use without arguments to open admin gump.")]
|
|
private static void FeatureFlag_OnCommand(CommandEventArgs e)
|
|
{
|
|
var from = e.Mobile;
|
|
|
|
if (e.Arguments.Length == 0)
|
|
{
|
|
from.SendGump(new FeatureFlagAdminGump());
|
|
return;
|
|
}
|
|
|
|
var flagKey = e.Arguments[0].ToLowerInvariant();
|
|
var action = e.Arguments.Length > 1 ? e.Arguments[1].ToLowerInvariant() : "info";
|
|
|
|
if (action is "on" or "enable" or "true" or "1")
|
|
{
|
|
if (FeatureFlagManager.SetFlag(flagKey, true, from.Name))
|
|
{
|
|
from.SendMessage(0x35, $"Feature flag '{flagKey}' has been ENABLED.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, $"Feature flag '{flagKey}' not found.");
|
|
}
|
|
}
|
|
else if (action is "off" or "disable" or "false" or "0")
|
|
{
|
|
if (FeatureFlagManager.SetFlag(flagKey, false, from.Name))
|
|
{
|
|
from.SendMessage(0x35, $"Feature flag '{flagKey}' has been DISABLED.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, $"Feature flag '{flagKey}' not found.");
|
|
}
|
|
}
|
|
else if (action == "toggle")
|
|
{
|
|
var flag = FeatureFlagManager.GetFlag(flagKey);
|
|
if (flag != null)
|
|
{
|
|
FeatureFlagManager.SetFlag(flagKey, !flag.Enabled, from.Name);
|
|
from.SendMessage(0x35, $"Feature flag '{flagKey}' toggled to {(flag.Enabled ? "DISABLED" : "ENABLED")}.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, $"Feature flag '{flagKey}' not found.");
|
|
}
|
|
}
|
|
else if (action == "create")
|
|
{
|
|
if (e.Arguments.Length < 4)
|
|
{
|
|
from.SendMessage("Usage: [FeatureFlag <key> create <category> <description>");
|
|
return;
|
|
}
|
|
|
|
var category = e.Arguments[2];
|
|
var description = string.Join(" ", e.Arguments, 3, e.Arguments.Length - 3);
|
|
FeatureFlagManager.CreateOrUpdateFlag(flagKey, description, category, true, from.Name);
|
|
from.SendMessage(0x35, $"Feature flag '{flagKey}' created.");
|
|
}
|
|
else if (action is "delete" or "remove")
|
|
{
|
|
if (FeatureFlagManager.RemoveFlag(flagKey, from.Name))
|
|
{
|
|
from.SendMessage(0x35, $"Feature flag '{flagKey}' removed.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, $"Feature flag '{flagKey}' not found.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var infoFlag = FeatureFlagManager.GetFlag(flagKey);
|
|
if (infoFlag != null)
|
|
{
|
|
from.SendMessage(0x35, $"=== Feature Flag: {infoFlag.Key} ===");
|
|
from.SendMessage($"Enabled: {(infoFlag.Enabled ? "Yes" : "No")}");
|
|
from.SendMessage($"Default: {(infoFlag.DefaultEnabled ? "Yes" : "No")}");
|
|
from.SendMessage($"Category: {infoFlag.Category}");
|
|
from.SendMessage($"Description: {infoFlag.Description}");
|
|
from.SendMessage($"Last Modified: {infoFlag.LastModified:G} by {infoFlag.LastModifiedBy}");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, $"Feature flag '{flagKey}' not found.");
|
|
from.SendMessage("Use [FeatureList to see all available flags.");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Usage("BlockGump <typeName> [reason]")]
|
|
[Description("Block a gump type from being displayed to players.")]
|
|
private static void BlockGump_OnCommand(CommandEventArgs e)
|
|
{
|
|
var from = e.Mobile;
|
|
|
|
if (e.Arguments.Length == 0)
|
|
{
|
|
from.SendMessage("Usage: [BlockGump <typeName> [reason]");
|
|
from.SendMessage("Example: [BlockGump CraftGump Crafting temporarily disabled");
|
|
return;
|
|
}
|
|
|
|
var typeName = e.Arguments[0];
|
|
var reason = e.Arguments.Length > 1
|
|
? string.Join(" ", e.Arguments, 1, e.Arguments.Length - 1)
|
|
: null;
|
|
|
|
if (FeatureFlagManager.BlockGumpByName(typeName, reason, from.Name))
|
|
{
|
|
from.SendMessage(0x35, $"Gump '{typeName}' has been BLOCKED.");
|
|
if (reason != null)
|
|
{
|
|
from.SendMessage($"Reason: {reason}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, $"Could not find gump type '{typeName}'.");
|
|
from.SendMessage("Make sure you're using the correct type name (e.g., CraftGump, HelpGump, etc.)");
|
|
}
|
|
}
|
|
|
|
[Usage("UnblockGump <typeName>")]
|
|
[Description("Remove a gump type block, allowing it to be displayed again.")]
|
|
private static void UnblockGump_OnCommand(CommandEventArgs e)
|
|
{
|
|
var from = e.Mobile;
|
|
|
|
if (e.Arguments.Length == 0)
|
|
{
|
|
from.SendMessage("Usage: [UnblockGump <typeName>");
|
|
from.SendMessage("Use [FeatureList gumps to see blocked gumps.");
|
|
return;
|
|
}
|
|
|
|
var typeName = e.Arguments[0];
|
|
|
|
if (FeatureFlagManager.UnblockGumpByName(typeName, from.Name))
|
|
{
|
|
from.SendMessage(0x35, $"Gump block for '{typeName}' has been REMOVED.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, $"No block found for gump '{typeName}'.");
|
|
}
|
|
}
|
|
|
|
[Usage("BlockItemUse <typeName|target> [reason]")]
|
|
[Description("Block an item type from being used by players.")]
|
|
private static void BlockItemUse_OnCommand(CommandEventArgs e) =>
|
|
HandleBlockItem(e, "Use", FeatureFlagManager.BlockItemUse, FeatureFlagManager.BlockItemUseByName);
|
|
|
|
[Usage("BlockItemEquip <typeName|target> [reason]")]
|
|
[Description("Block an item type from being equipped by players.")]
|
|
private static void BlockItemEquip_OnCommand(CommandEventArgs e) =>
|
|
HandleBlockItem(e, "Equip", FeatureFlagManager.BlockItemEquip, FeatureFlagManager.BlockItemEquipByName);
|
|
|
|
[Usage("BlockItemContainer <typeName|target> [reason]")]
|
|
[Description("Block a container type from being opened by players.")]
|
|
private static void BlockItemContainer_OnCommand(CommandEventArgs e) =>
|
|
HandleBlockItem(e, "Container", FeatureFlagManager.BlockItemContainer, FeatureFlagManager.BlockItemContainerByName);
|
|
|
|
private static void HandleBlockItem(
|
|
CommandEventArgs e, string action,
|
|
Action<Type, string, string> blockByType,
|
|
Func<string, string, string, bool> blockByName)
|
|
{
|
|
var from = e.Mobile;
|
|
|
|
if (e.Arguments.Length == 0)
|
|
{
|
|
from.SendMessage($"Usage: [BlockItem{action} <typeName> [reason]");
|
|
from.SendMessage($"Or target an item: [BlockItem{action} target [reason]");
|
|
return;
|
|
}
|
|
|
|
if (e.Arguments[0].Equals("target", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var reason = e.Arguments.Length > 1
|
|
? string.Join(" ", e.Arguments, 1, e.Arguments.Length - 1)
|
|
: null;
|
|
|
|
from.SendMessage("Target the item to block:");
|
|
from.Target = new BlockItemTarget(action, reason, blockByType);
|
|
return;
|
|
}
|
|
|
|
var typeName = e.Arguments[0];
|
|
var blockReason = e.Arguments.Length > 1
|
|
? string.Join(" ", e.Arguments, 1, e.Arguments.Length - 1)
|
|
: null;
|
|
|
|
if (blockByName(typeName, blockReason, from.Name))
|
|
{
|
|
from.SendMessage(0x35, $"Item '{typeName}' {action} has been BLOCKED.");
|
|
if (blockReason != null)
|
|
{
|
|
from.SendMessage($"Reason: {blockReason}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, $"Could not find item type '{typeName}'.");
|
|
}
|
|
}
|
|
|
|
private sealed class BlockItemTarget : Target
|
|
{
|
|
private readonly string _action;
|
|
private readonly string _reason;
|
|
private readonly Action<Type, string, string> _blockByType;
|
|
|
|
public BlockItemTarget(string action, string reason, Action<Type, string, string> blockByType)
|
|
: base(-1, false, TargetFlags.None)
|
|
{
|
|
_action = action;
|
|
_reason = reason;
|
|
_blockByType = blockByType;
|
|
}
|
|
|
|
protected override void OnTarget(Mobile from, object targeted)
|
|
{
|
|
if (targeted is Item item)
|
|
{
|
|
var type = item.GetType();
|
|
_blockByType(type, _reason, from.Name);
|
|
from.SendMessage(0x35, $"Item '{type.Name}' {_action} has been BLOCKED.");
|
|
if (_reason != null)
|
|
{
|
|
from.SendMessage($"Reason: {_reason}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, "That is not an item.");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Usage("UnblockItemUse <typeName>")]
|
|
[Description("Remove the use block for an item type.")]
|
|
private static void UnblockItemUse_OnCommand(CommandEventArgs e) =>
|
|
HandleUnblockItem(e, "Use", FeatureFlagManager.UnblockItemUseByName);
|
|
|
|
[Usage("UnblockItemEquip <typeName>")]
|
|
[Description("Remove the equip block for an item type.")]
|
|
private static void UnblockItemEquip_OnCommand(CommandEventArgs e) =>
|
|
HandleUnblockItem(e, "Equip", FeatureFlagManager.UnblockItemEquipByName);
|
|
|
|
[Usage("UnblockItemContainer <typeName>")]
|
|
[Description("Remove the container access block for an item type.")]
|
|
private static void UnblockItemContainer_OnCommand(CommandEventArgs e) =>
|
|
HandleUnblockItem(e, "Container", FeatureFlagManager.UnblockItemContainerByName);
|
|
|
|
private static void HandleUnblockItem(
|
|
CommandEventArgs e, string action, Func<string, string, bool> unblockByName)
|
|
{
|
|
var from = e.Mobile;
|
|
|
|
if (e.Arguments.Length == 0)
|
|
{
|
|
from.SendMessage($"Usage: [UnblockItem{action} <typeName>");
|
|
from.SendMessage("Use [FeatureList items to see blocked items.");
|
|
return;
|
|
}
|
|
|
|
var typeName = e.Arguments[0];
|
|
|
|
if (unblockByName(typeName, from.Name))
|
|
{
|
|
from.SendMessage(0x35, $"Item '{typeName}' {action} block has been REMOVED.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, $"No {action} block found for item '{typeName}'.");
|
|
}
|
|
}
|
|
|
|
[Usage("BlockSkill <SkillName> [reason]")]
|
|
[Description("Block a skill from being used by players.")]
|
|
private static void BlockSkill_OnCommand(CommandEventArgs e)
|
|
{
|
|
var from = e.Mobile;
|
|
|
|
if (e.Arguments.Length == 0)
|
|
{
|
|
from.SendMessage("Usage: [BlockSkill <SkillName> [reason]");
|
|
from.SendMessage("Example: [BlockSkill Magery Investigating exploit");
|
|
return;
|
|
}
|
|
|
|
if (!Enum.TryParse<SkillName>(e.Arguments[0], true, out var skill))
|
|
{
|
|
from.SendMessage(0x22, $"Unknown skill name '{e.Arguments[0]}'.");
|
|
from.SendMessage("Valid skills: Alchemy, Anatomy, Magery, Mining, etc.");
|
|
return;
|
|
}
|
|
|
|
var reason = e.Arguments.Length > 1
|
|
? string.Join(" ", e.Arguments, 1, e.Arguments.Length - 1)
|
|
: null;
|
|
|
|
FeatureFlagManager.BlockSkill(skill, reason, from.Name);
|
|
from.SendMessage(0x35, $"Skill '{skill}' has been BLOCKED.");
|
|
if (reason != null)
|
|
{
|
|
from.SendMessage($"Reason: {reason}");
|
|
}
|
|
}
|
|
|
|
[Usage("UnblockSkill <SkillName>")]
|
|
[Description("Remove a skill block.")]
|
|
private static void UnblockSkill_OnCommand(CommandEventArgs e)
|
|
{
|
|
var from = e.Mobile;
|
|
|
|
if (e.Arguments.Length == 0)
|
|
{
|
|
from.SendMessage("Usage: [UnblockSkill <SkillName>");
|
|
from.SendMessage("Use [FeatureList skills to see blocked skills.");
|
|
return;
|
|
}
|
|
|
|
if (!Enum.TryParse<SkillName>(e.Arguments[0], true, out var skill))
|
|
{
|
|
from.SendMessage(0x22, $"Unknown skill name '{e.Arguments[0]}'.");
|
|
return;
|
|
}
|
|
|
|
if (FeatureFlagManager.UnblockSkill(skill, from.Name))
|
|
{
|
|
from.SendMessage(0x35, $"Skill block for '{skill}' has been REMOVED.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, $"No block found for skill '{skill}'.");
|
|
}
|
|
}
|
|
|
|
[Usage("BlockSpell <SpellTypeName> [reason]")]
|
|
[Description("Block a spell from being cast by players.")]
|
|
private static void BlockSpell_OnCommand(CommandEventArgs e)
|
|
{
|
|
var from = e.Mobile;
|
|
|
|
if (e.Arguments.Length == 0)
|
|
{
|
|
from.SendMessage("Usage: [BlockSpell <SpellTypeName> [reason]");
|
|
from.SendMessage("Example: [BlockSpell RecallSpell Investigating exploit");
|
|
return;
|
|
}
|
|
|
|
var typeName = e.Arguments[0];
|
|
var reason = e.Arguments.Length > 1
|
|
? string.Join(" ", e.Arguments, 1, e.Arguments.Length - 1)
|
|
: null;
|
|
|
|
if (FeatureFlagManager.BlockSpellByName(typeName, reason, from.Name))
|
|
{
|
|
from.SendMessage(0x35, $"Spell '{typeName}' has been BLOCKED.");
|
|
if (reason != null)
|
|
{
|
|
from.SendMessage($"Reason: {reason}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, $"Could not find spell type '{typeName}'.");
|
|
from.SendMessage("Make sure you're using the correct type name (e.g., RecallSpell, GateTravelSpell, etc.)");
|
|
}
|
|
}
|
|
|
|
[Usage("UnblockSpell <SpellTypeName>")]
|
|
[Description("Remove a spell block.")]
|
|
private static void UnblockSpell_OnCommand(CommandEventArgs e)
|
|
{
|
|
var from = e.Mobile;
|
|
|
|
if (e.Arguments.Length == 0)
|
|
{
|
|
from.SendMessage("Usage: [UnblockSpell <SpellTypeName>");
|
|
from.SendMessage("Use [FeatureList spells to see blocked spells.");
|
|
return;
|
|
}
|
|
|
|
var typeName = e.Arguments[0];
|
|
|
|
if (FeatureFlagManager.UnblockSpellByName(typeName, from.Name))
|
|
{
|
|
from.SendMessage(0x35, $"Spell block for '{typeName}' has been REMOVED.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, $"No block found for spell '{typeName}'.");
|
|
}
|
|
}
|
|
|
|
[Usage("FeatureList [flags|gumps|items|skills|spells|all]")]
|
|
[Description("List all feature flags and blocks.")]
|
|
private static void FeatureList_OnCommand(CommandEventArgs e)
|
|
{
|
|
var from = e.Mobile;
|
|
var filter = e.Arguments.Length > 0 ? e.Arguments[0].ToLowerInvariant() : "all";
|
|
|
|
if (filter is "flags" or "all")
|
|
{
|
|
var flags = new List<FeatureFlag>(FeatureFlagManager.GetAllFlags());
|
|
flags.Sort((a, b) =>
|
|
{
|
|
var cmp = string.Compare(a.Category, b.Category, StringComparison.OrdinalIgnoreCase);
|
|
return cmp != 0 ? cmp : string.Compare(a.Key, b.Key, StringComparison.OrdinalIgnoreCase);
|
|
});
|
|
from.SendMessage(0x35, $"=== Feature Flags ({flags.Count}) ===");
|
|
foreach (var flag in flags)
|
|
{
|
|
var status = flag.Enabled ? "[ON]" : "[OFF]";
|
|
from.SendMessage($" {status} {flag.Key} ({flag.Category}): {flag.Description}");
|
|
}
|
|
}
|
|
|
|
if (filter is "gumps" or "all")
|
|
{
|
|
var gumpBlocks = FeatureFlagManager.GetAllGumpBlocks();
|
|
from.SendMessage(0x35, $"=== Blocked Gumps ({gumpBlocks.Count}) ===");
|
|
foreach (var block in gumpBlocks)
|
|
{
|
|
var status = block.Active ? "[OFF]" : "[ON]";
|
|
from.SendMessage($" {status} {block.DisplayName}: {block.Reason ?? FeatureFlagSettings.DefaultGumpBlockedMessage}");
|
|
}
|
|
}
|
|
|
|
if (filter is "items" or "all")
|
|
{
|
|
var itemBlocks = FeatureFlagManager.GetAllItemBlocks();
|
|
from.SendMessage(0x35, $"=== Blocked Items ({itemBlocks.Count}) ===");
|
|
foreach (var block in itemBlocks)
|
|
{
|
|
var actions = new List<string>(3);
|
|
if (block.BlockUse) actions.Add("Use");
|
|
if (block.BlockEquip) actions.Add("Equip");
|
|
if (block.BlockContainerAccess) actions.Add("Container");
|
|
|
|
var actionsStr = actions.Count > 0 ? $"[{string.Join("][", actions)}]" : "[none]";
|
|
var status = block.Active ? "[OFF]" : "[ON]";
|
|
from.SendMessage($" {status} {block.DisplayName} {actionsStr}: {block.Reason ?? FeatureFlagSettings.DefaultItemUseBlockedMessage}");
|
|
}
|
|
}
|
|
|
|
if (filter is "skills" or "all")
|
|
{
|
|
var skillBlocks = FeatureFlagManager.GetAllSkillBlocks();
|
|
from.SendMessage(0x35, "=== Blocked Skills ===");
|
|
for (var i = 0; i < skillBlocks.Length; i++)
|
|
{
|
|
var block = skillBlocks[i];
|
|
if (block == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var status = block.Active ? "[OFF]" : "[ON]";
|
|
from.SendMessage($" {status} {block.DisplayName}: {block.Reason ?? FeatureFlagSettings.DefaultSkillDisabledMessage}");
|
|
}
|
|
}
|
|
|
|
if (filter is "spells" or "all")
|
|
{
|
|
var spellBlocks = FeatureFlagManager.GetAllSpellBlocks();
|
|
from.SendMessage(0x35, "=== Blocked Spells ===");
|
|
foreach (var block in spellBlocks)
|
|
{
|
|
if (block == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var status = block.Active ? "[OFF]" : "[ON]";
|
|
from.SendMessage($" {status} {block.DisplayName}: {block.Reason ?? FeatureFlagSettings.DefaultSpellDisabledMessage}");
|
|
}
|
|
}
|
|
|
|
if (filter != "flags" && filter != "gumps" && filter != "items" && filter != "skills" && filter != "spells" && filter != "all")
|
|
{
|
|
from.SendMessage("Usage: [FeatureList [flags|gumps|items|skills|spells|all]");
|
|
}
|
|
}
|
|
|
|
[Usage("FeatureAdmin")]
|
|
[Description("Open the feature flag administration gump.")]
|
|
private static void FeatureAdmin_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.SendGump(new FeatureFlagAdminGump());
|
|
}
|
|
|
|
[Usage("ListGumps")]
|
|
[Description("List all open gumps for yourself or a targeted player. Useful for finding gump names to block.")]
|
|
private static void ListGumps_OnCommand(CommandEventArgs e)
|
|
{
|
|
var from = e.Mobile;
|
|
|
|
if (e.Arguments.Length > 0 && e.Arguments[0].Equals("self", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ListGumpsFor(from, from);
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("Target a player to list their open gumps (or use [ListGumps self):");
|
|
from.Target = new ListGumpsTarget();
|
|
}
|
|
}
|
|
|
|
private static void ListGumpsFor(Mobile from, Mobile target)
|
|
{
|
|
if (target?.NetState == null)
|
|
{
|
|
from.SendMessage(0x22, "That player is not online.");
|
|
return;
|
|
}
|
|
|
|
var gumps = target.GetGumps();
|
|
var count = 0;
|
|
var gumpList = new List<(string shortName, string fullName)>();
|
|
|
|
foreach (var gump in gumps)
|
|
{
|
|
var type = gump.GetType();
|
|
var fullName = type.FullName ?? type.Name;
|
|
var shortName = type.Name;
|
|
gumpList.Add((shortName, fullName));
|
|
count++;
|
|
}
|
|
|
|
if (count == 0)
|
|
{
|
|
from.SendMessage(0x35, $"{target.Name} has no gumps open.");
|
|
return;
|
|
}
|
|
|
|
from.SendMessage(0x35, $"=== Open Gumps for {target.Name} ({count}) ===");
|
|
foreach (var (shortName, fullName) in gumpList)
|
|
{
|
|
from.SendMessage($" • {shortName}");
|
|
from.SendMessage($" Full: {fullName}");
|
|
from.SendMessage(0x3B2, $" Block with: [BlockGump {shortName}");
|
|
}
|
|
}
|
|
|
|
private sealed class ListGumpsTarget : Target
|
|
{
|
|
public ListGumpsTarget() : base(-1, false, TargetFlags.None)
|
|
{
|
|
}
|
|
|
|
protected override void OnTarget(Mobile from, object targeted)
|
|
{
|
|
if (targeted is Mobile m)
|
|
{
|
|
ListGumpsFor(from, m);
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(0x22, "That is not a player.");
|
|
}
|
|
}
|
|
}
|
|
}
|