feat: Adds Feature Flag System (#2328)
## 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
This commit is contained in:
parent
e4178bb495
commit
b191498569
25 changed files with 2461 additions and 117 deletions
|
|
@ -1019,6 +1019,8 @@ public ref struct PooledRefList<T>
|
|||
return array;
|
||||
}
|
||||
|
||||
public ReadOnlySpan<T> AsSpan() => _items.AsSpan(0, _size);
|
||||
|
||||
// Sets the capacity of this list to the size of the list. This method can
|
||||
// be used to minimize a list's memory overhead once it is known that no
|
||||
// new elements will be added to the list. To completely clear a list and
|
||||
|
|
|
|||
12
Projects/Server/FeatureFlags.cs
Normal file
12
Projects/Server/FeatureFlags.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace Server;
|
||||
|
||||
/// <summary>
|
||||
/// Static boolean flags for Server project hot paths.
|
||||
/// Values are synced by FeatureFlagManager in UOContent when flags change.
|
||||
/// </summary>
|
||||
public static class ServerFeatureFlags
|
||||
{
|
||||
public static bool PlayerTrading { get; set; } = true;
|
||||
public static bool PvPCombat { get; set; } = true;
|
||||
public static bool BankAccess { get; set; } = true;
|
||||
}
|
||||
|
|
@ -28,6 +28,12 @@ public partial class BankBox : Container
|
|||
|
||||
public void Open()
|
||||
{
|
||||
if (!ServerFeatureFlags.BankAccess && Owner?.AccessLevel < AccessLevel.Administrator)
|
||||
{
|
||||
Owner.SendMessage(0x22, "Bank access is temporarily disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
Opened = true;
|
||||
|
||||
if (Owner != null)
|
||||
|
|
|
|||
|
|
@ -7487,6 +7487,12 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
|
||||
public virtual bool OpenTrade(Mobile from, Item offer = null)
|
||||
{
|
||||
if (!ServerFeatureFlags.PlayerTrading)
|
||||
{
|
||||
from.SendMessage(0x22, "Player trading is temporarily disabled.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!from.Player || !Player || !from.Alive || !Alive)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -8222,6 +8228,16 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!ServerFeatureFlags.PvPCombat && Player && target.Player)
|
||||
{
|
||||
if (message)
|
||||
{
|
||||
SendLocalizedMessage(1001018); // You can not perform negative acts on your target.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Pets
|
||||
if (!Region.AllowHarmful(this, target))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ public static class World
|
|||
NetState.FlushAll();
|
||||
}
|
||||
|
||||
public static void BroadcastStaff(string text) => BroadcastStaff(0x35, false, text);
|
||||
|
||||
public static void BroadcastStaff(int hue, bool ascii, string text)
|
||||
{
|
||||
var length = OutgoingMessagePackets.GetMaxMessageLength(text);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using ModernUO.Serialization;
|
||||
using Server.Gumps;
|
||||
using Server.Mobiles;
|
||||
using Server.Systems.FeatureFlags;
|
||||
|
||||
namespace Server.Engines.BulkOrders
|
||||
{
|
||||
|
|
@ -75,14 +76,19 @@ namespace Server.Engines.BulkOrders
|
|||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (IsChildOf(from.Backpack) || InSecureTrade || RootParent is PlayerVendor)
|
||||
{
|
||||
from.SendGump(new LargeBODGump(this));
|
||||
}
|
||||
else
|
||||
if (!(IsChildOf(from.Backpack) || InSecureTrade || RootParent is PlayerVendor))
|
||||
{
|
||||
from.SendLocalizedMessage(1045156); // You must have the deed in your backpack to use it.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ContentFeatureFlags.BulkOrders && from.AccessLevel < AccessLevel.Administrator)
|
||||
{
|
||||
from.SendMessage(0x22, "Bulk orders are temporarily disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendGump(new LargeBODGump(this));
|
||||
}
|
||||
|
||||
public override void EndCombine(Mobile from, Item item)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using ModernUO.Serialization;
|
|||
using Server.Gumps;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Systems.FeatureFlags;
|
||||
|
||||
namespace Server.Engines.BulkOrders;
|
||||
|
||||
|
|
@ -69,14 +70,19 @@ public abstract partial class SmallBOD : BaseBOD
|
|||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (IsChildOf(from.Backpack) || InSecureTrade || RootParent is PlayerVendor)
|
||||
{
|
||||
from.SendGump(new SmallBODGump(this));
|
||||
}
|
||||
else
|
||||
if (!(IsChildOf(from.Backpack) || InSecureTrade || RootParent is PlayerVendor))
|
||||
{
|
||||
from.SendLocalizedMessage(1045156); // You must have the deed in your backpack to use it.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ContentFeatureFlags.BulkOrders && from.AccessLevel < AccessLevel.Administrator)
|
||||
{
|
||||
from.SendMessage(0x22, "Bulk orders are temporarily disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendGump(new SmallBODGump(this));
|
||||
}
|
||||
|
||||
public override void OnDoubleClickNotAccessible(Mobile from)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
namespace Server.Systems.FeatureFlags;
|
||||
|
||||
/// <summary>
|
||||
/// Static boolean flags for UOContent hot paths.
|
||||
/// Values are synced by FeatureFlagManager when flags change.
|
||||
/// </summary>
|
||||
public static class ContentFeatureFlags
|
||||
{
|
||||
public static bool VendorPurchase { get; set; } = true;
|
||||
public static bool VendorSell { get; set; } = true;
|
||||
public static bool PlayerVendors { get; set; } = true;
|
||||
public static bool HousePlacement { get; set; } = true;
|
||||
public static bool BoatPlacement { get; set; } = true;
|
||||
public static bool BulkOrders { get; set; } = true;
|
||||
}
|
||||
464
Projects/UOContent/Engines/FeatureFlags/FeatureFlagAdminGump.cs
Normal file
464
Projects/UOContent/Engines/FeatureFlags/FeatureFlagAdminGump.cs
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Collections;
|
||||
using Server.Gumps;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server.Systems.FeatureFlags;
|
||||
|
||||
public sealed class FeatureFlagAdminGump : DynamicGump
|
||||
{
|
||||
public enum FeatureFlagPage
|
||||
{
|
||||
Flags,
|
||||
GumpBlocks,
|
||||
ItemBlocks,
|
||||
SkillBlocks,
|
||||
SpellBlocks
|
||||
}
|
||||
|
||||
private FeatureFlagPage _currentPage;
|
||||
private int _pageIndex;
|
||||
private int _displayedCount;
|
||||
private readonly FeatureFlag[] _displayedFlags = new FeatureFlag[FlagsPerPage];
|
||||
private readonly FeatureFlagBlockEntry[] _displayedBlocks = new FeatureFlagBlockEntry[BlocksPerPage];
|
||||
private const int FlagsPerPage = 10;
|
||||
private const int BlocksPerPage = 7;
|
||||
private const int FlagRowHeight = 25;
|
||||
private const int BlockRowHeight = 45;
|
||||
|
||||
public FeatureFlagAdminGump(FeatureFlagPage page = FeatureFlagPage.Flags, int pageIndex = 0) : base(50, 50)
|
||||
{
|
||||
_currentPage = page;
|
||||
_pageIndex = pageIndex;
|
||||
}
|
||||
|
||||
private void Resend(Mobile from, FeatureFlagPage page, int pageIndex = 0)
|
||||
{
|
||||
_currentPage = page;
|
||||
_pageIndex = pageIndex;
|
||||
from.SendGump(this);
|
||||
}
|
||||
|
||||
protected override void BuildLayout(ref DynamicGumpBuilder builder)
|
||||
{
|
||||
builder.AddPage();
|
||||
|
||||
// Background
|
||||
builder.AddBackground(0, 0, 820, 500, 9270);
|
||||
|
||||
// Title
|
||||
builder.AddHtml(0, 15, 820, 25, "Feature Flag Administration".Center("#00FF00"));
|
||||
|
||||
// Tab buttons
|
||||
var flagsColor = _currentPage == FeatureFlagPage.Flags ? GumpTextColors.Yellow : GumpTextColors.White;
|
||||
var gumpsColor = _currentPage == FeatureFlagPage.GumpBlocks ? GumpTextColors.Yellow : GumpTextColors.White;
|
||||
var itemsColor = _currentPage == FeatureFlagPage.ItemBlocks ? GumpTextColors.Yellow : GumpTextColors.White;
|
||||
var skillsColor = _currentPage == FeatureFlagPage.SkillBlocks ? GumpTextColors.Yellow : GumpTextColors.White;
|
||||
var spellsColor = _currentPage == FeatureFlagPage.SpellBlocks ? GumpTextColors.Yellow : GumpTextColors.White;
|
||||
|
||||
builder.AddButton(20, 45, 4005, 4007, 1);
|
||||
builder.AddHtml(55, 47, 80, 20, "Flags".Color(flagsColor));
|
||||
|
||||
builder.AddButton(140, 45, 4005, 4007, 2);
|
||||
builder.AddHtml(175, 47, 100, 20, "Gumps".Color(gumpsColor));
|
||||
|
||||
builder.AddButton(270, 45, 4005, 4007, 3);
|
||||
builder.AddHtml(305, 47, 80, 20, "Items".Color(itemsColor));
|
||||
|
||||
builder.AddButton(390, 45, 4005, 4007, 4);
|
||||
builder.AddHtml(425, 47, 80, 20, "Skills".Color(skillsColor));
|
||||
|
||||
builder.AddButton(510, 45, 4005, 4007, 5);
|
||||
builder.AddHtml(545, 47, 80, 20, "Spells".Color(spellsColor));
|
||||
|
||||
// Content area
|
||||
builder.AddAlphaRegion(15, 75, 790, 380);
|
||||
|
||||
if (_currentPage == FeatureFlagPage.Flags)
|
||||
{
|
||||
BuildFlagsPage(ref builder);
|
||||
}
|
||||
else if (_currentPage == FeatureFlagPage.GumpBlocks)
|
||||
{
|
||||
BuildBlockPage(
|
||||
ref builder,
|
||||
"Gump Type",
|
||||
FeatureFlagManager.GetAllGumpBlocks(),
|
||||
"Use [BlockGump to add new blocks"
|
||||
);
|
||||
}
|
||||
else if (_currentPage == FeatureFlagPage.ItemBlocks)
|
||||
{
|
||||
BuildItemBlockPage(
|
||||
ref builder,
|
||||
FeatureFlagManager.GetAllItemBlocks(),
|
||||
"Use [BlockItem to add new blocks"
|
||||
);
|
||||
}
|
||||
else if (_currentPage == FeatureFlagPage.SkillBlocks)
|
||||
{
|
||||
BuildBlockPage(
|
||||
ref builder,
|
||||
"Skill",
|
||||
FeatureFlagManager.GetAllSkillBlocks(),
|
||||
"Use [BlockSkill to add new blocks"
|
||||
);
|
||||
}
|
||||
else if (_currentPage == FeatureFlagPage.SpellBlocks)
|
||||
{
|
||||
BuildBlockPage(
|
||||
ref builder,
|
||||
"Spell Type",
|
||||
FeatureFlagManager.GetAllSpellBlocks(),
|
||||
"Use [BlockSpell to add new blocks"
|
||||
);
|
||||
}
|
||||
|
||||
// Close button
|
||||
builder.AddButton(770, 460, 4017, 4019, 0);
|
||||
builder.AddHtml(720, 462, 50, 20, "Close".Color(GumpTextColors.White));
|
||||
|
||||
// Save button
|
||||
builder.AddButton(20, 460, 4023, 4025, 100);
|
||||
builder.AddHtml(55, 462, 50, 20, "Save".Color(GumpTextColors.White));
|
||||
|
||||
// Refresh button
|
||||
builder.AddButton(120, 460, 4014, 4016, 101);
|
||||
builder.AddHtml(155, 462, 60, 20, "Refresh".Color(GumpTextColors.White));
|
||||
}
|
||||
|
||||
private void BuildFlagsPage(ref DynamicGumpBuilder builder)
|
||||
{
|
||||
builder.AddHtml(20, 80, 150, 20, "Flag".Color(GumpTextColors.White));
|
||||
builder.AddHtml(180, 80, 150, 20, "Category".Color(GumpTextColors.White));
|
||||
builder.AddHtml(275, 80, 350, 20, "Description".Color(GumpTextColors.White));
|
||||
builder.AddHtml(690, 80, 60, 20, "Status".Color(GumpTextColors.White));
|
||||
|
||||
var flags = new List<FeatureFlag>(FeatureFlagManager.GetAllFlags());
|
||||
flags.Sort((a, b) =>
|
||||
{
|
||||
var cmp = a.Category.InsensitiveCompare(b.Category);
|
||||
return cmp != 0 ? cmp : a.Key.InsensitiveCompare(b.Key);
|
||||
});
|
||||
|
||||
var startIndex = _pageIndex * FlagsPerPage;
|
||||
var endIndex = Math.Min(startIndex + FlagsPerPage, flags.Count);
|
||||
var totalPages = Math.Max(1, (int)Math.Ceiling(flags.Count / (double)FlagsPerPage));
|
||||
|
||||
_displayedCount = 0;
|
||||
var y = 105;
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
var flag = flags[i];
|
||||
_displayedFlags[_displayedCount] = flag;
|
||||
var statusColor = flag.Enabled ? GumpTextColors.Green : GumpTextColors.Red;
|
||||
|
||||
builder.AddButton(20, y, flag.Enabled ? 2154 : 2151, flag.Enabled ? 2151 : 2154, 1000 + _displayedCount);
|
||||
builder.AddHtml(60, y + 3, 130, 20, flag.Key.Color(GumpTextColors.White));
|
||||
builder.AddHtml(180, y + 3, 150, 20, (flag.Category ?? "").Color(GumpTextColors.LightGray));
|
||||
builder.AddHtml(275, y + 3, 350, 20, (flag.Description ?? "").Color(GumpTextColors.LightGray));
|
||||
builder.AddHtml(690, y + 3, 60, 20, (flag.Enabled ? "ON" : "OFF").Color(statusColor));
|
||||
|
||||
_displayedCount++;
|
||||
y += FlagRowHeight;
|
||||
}
|
||||
|
||||
AddPagination(ref builder, totalPages);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void BuildBlockPage(
|
||||
ref DynamicGumpBuilder builder,
|
||||
string headerLabel,
|
||||
IReadOnlyCollection<FeatureFlagBlockEntry> blocks,
|
||||
string helpText
|
||||
)
|
||||
{
|
||||
using var list = PooledRefList<FeatureFlagBlockEntry>.Create();
|
||||
foreach (var b in blocks)
|
||||
{
|
||||
if (b != null)
|
||||
{
|
||||
list.Add(b);
|
||||
}
|
||||
}
|
||||
BuildBlockPage(ref builder, headerLabel, list.AsSpan(), helpText);
|
||||
}
|
||||
|
||||
private void BuildBlockPage(
|
||||
ref DynamicGumpBuilder builder,
|
||||
string headerLabel,
|
||||
ReadOnlySpan<FeatureFlagBlockEntry> blocks,
|
||||
string helpText
|
||||
)
|
||||
{
|
||||
builder.AddHtml(20, 80, 180, 20, headerLabel.Color(GumpTextColors.White));
|
||||
builder.AddHtml(200, 80, 400, 20, "Reason".Color(GumpTextColors.White));
|
||||
builder.AddHtml(690, 80, 60, 20, "Status".Color(GumpTextColors.White));
|
||||
builder.AddHtml(750, 80, 60, 20, "Remove".Color(GumpTextColors.White));
|
||||
|
||||
var startIndex = _pageIndex * BlocksPerPage;
|
||||
var count = 0;
|
||||
|
||||
_displayedCount = 0;
|
||||
var skipped = 0;
|
||||
var y = 105;
|
||||
|
||||
for (var i = 0; i < blocks.Length; i++)
|
||||
{
|
||||
var block = blocks[i];
|
||||
if (block == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
count++;
|
||||
|
||||
if (skipped < startIndex)
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_displayedCount < BlocksPerPage)
|
||||
{
|
||||
_displayedBlocks[_displayedCount] = block;
|
||||
var statusColor = block.Active ? GumpTextColors.Red : GumpTextColors.Green;
|
||||
|
||||
builder.AddButton(20, y, block.Active ? 2151 : 2154, block.Active ? 2154 : 2151, 2000 + _displayedCount);
|
||||
builder.AddHtml(60, y + 3, 150, 20, block.DisplayName.Color(GumpTextColors.White));
|
||||
builder.AddHtml(200, y + 3, 400, 40, (block.Reason ?? "(default)").Color(GumpTextColors.LightGray));
|
||||
builder.AddHtml(690, y + 3, 60, 20, (block.Active ? "OFF" : "ON").Color(statusColor));
|
||||
builder.AddButton(750, y + 3, 4017, 4019, 3000 + _displayedCount);
|
||||
|
||||
_displayedCount++;
|
||||
y += BlockRowHeight;
|
||||
}
|
||||
}
|
||||
|
||||
var totalPages = Math.Max(1, (int)Math.Ceiling(count / (double)BlocksPerPage));
|
||||
AddPagination(ref builder, totalPages);
|
||||
builder.AddHtml(20, 430, 400, 20, helpText.Color(GumpTextColors.LightGray));
|
||||
}
|
||||
|
||||
private void BuildItemBlockPage(
|
||||
ref DynamicGumpBuilder builder,
|
||||
IReadOnlyCollection<ItemBlockEntry> blocks,
|
||||
string helpText
|
||||
)
|
||||
{
|
||||
builder.AddHtml(20, 80, 150, 20, "Item Type".Color(GumpTextColors.White));
|
||||
builder.AddHtml(180, 80, 280, 20, "Reason".Color(GumpTextColors.White));
|
||||
builder.AddHtml(530, 80, 50, 20, "Use".Color(GumpTextColors.White));
|
||||
builder.AddHtml(580, 80, 50, 20, "Equip".Color(GumpTextColors.White));
|
||||
builder.AddHtml(640, 80, 50, 20, "Open".Color(GumpTextColors.White));
|
||||
builder.AddHtml(700, 80, 40, 20, "Edit".Color(GumpTextColors.White));
|
||||
builder.AddHtml(750, 80, 60, 20, "Remove".Color(GumpTextColors.White));
|
||||
|
||||
var startIndex = _pageIndex * BlocksPerPage;
|
||||
|
||||
_displayedCount = 0;
|
||||
var skipped = 0;
|
||||
var y = 105;
|
||||
|
||||
foreach (var block in blocks)
|
||||
{
|
||||
if (skipped < startIndex)
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_displayedCount >= BlocksPerPage)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
_displayedBlocks[_displayedCount] = block;
|
||||
|
||||
builder.AddButton(20, y, block.Active ? 2151 : 2154, block.Active ? 2154 : 2151, 2000 + _displayedCount);
|
||||
builder.AddHtml(60, y + 3, 120, 20, block.DisplayName.Color(GumpTextColors.White));
|
||||
builder.AddHtml(180, y + 3, 280, 40, (block.Reason ?? "(default)").Color(GumpTextColors.LightGray));
|
||||
|
||||
var useColor = block.BlockUse ? GumpTextColors.Red : GumpTextColors.Green;
|
||||
var equipColor = block.BlockEquip ? GumpTextColors.Red : GumpTextColors.Green;
|
||||
var containerColor = block.BlockContainerAccess ? GumpTextColors.Red : GumpTextColors.Green;
|
||||
|
||||
builder.AddHtml(530, y + 3, 50, 20, (block.BlockUse ? "X" : "-").Color(useColor));
|
||||
builder.AddHtml(580, y + 3, 50, 20, (block.BlockEquip ? "X" : "-").Color(equipColor));
|
||||
builder.AddHtml(640, y + 3, 50, 20, (block.BlockContainerAccess ? "X" : "-").Color(containerColor));
|
||||
|
||||
builder.AddButton(700, y + 3, 4011, 4013, 4000 + _displayedCount);
|
||||
builder.AddButton(750, y + 3, 4017, 4019, 3000 + _displayedCount);
|
||||
|
||||
_displayedCount++;
|
||||
y += BlockRowHeight;
|
||||
}
|
||||
|
||||
var totalCount = blocks.Count;
|
||||
var totalPages = Math.Max(1, (int)Math.Ceiling(totalCount / (double)BlocksPerPage));
|
||||
AddPagination(ref builder, totalPages);
|
||||
builder.AddHtml(20, 430, 400, 20, helpText.Color(GumpTextColors.LightGray));
|
||||
}
|
||||
|
||||
private void AddPagination(ref DynamicGumpBuilder builder, int totalPages)
|
||||
{
|
||||
if (totalPages <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pageIndex > 0)
|
||||
{
|
||||
builder.AddButton(300, 425, 4014, 4016, 102);
|
||||
builder.AddHtml(335, 425, 50, 20, "Prev".Color(GumpTextColors.White));
|
||||
}
|
||||
|
||||
builder.AddHtml(370, 425, 60, 20, $"{_pageIndex + 1}/{totalPages}".Color(GumpTextColors.White));
|
||||
|
||||
if (_pageIndex < totalPages - 1)
|
||||
{
|
||||
builder.AddButton(420, 425, 4005, 4007, 103);
|
||||
builder.AddHtml(455, 425, 50, 20, "Next".Color(GumpTextColors.White));
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState sender, in RelayInfo info)
|
||||
{
|
||||
var from = sender.Mobile;
|
||||
var buttonId = info.ButtonID;
|
||||
|
||||
if (buttonId == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonId is >= (int)(FeatureFlagPage.Flags + 1) and <= (int)(FeatureFlagPage.SpellBlocks + 1))
|
||||
{
|
||||
Resend(from, (FeatureFlagPage)(buttonId - 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonId == 100)
|
||||
{
|
||||
FeatureFlagManager.Save();
|
||||
from.SendMessage(0x35, "Feature flags saved to disk.");
|
||||
from.SendGump(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonId == 101)
|
||||
{
|
||||
from.SendGump(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonId == 102)
|
||||
{
|
||||
Resend(from, _currentPage, _pageIndex - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonId == 103)
|
||||
{
|
||||
Resend(from, _currentPage, _pageIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (buttonId)
|
||||
{
|
||||
// Toggle feature flags
|
||||
case >= 1000 and < 1000 + FlagsPerPage:
|
||||
{
|
||||
var index = buttonId - 1000;
|
||||
if (index < _displayedCount)
|
||||
{
|
||||
var flag = _displayedFlags[index];
|
||||
FeatureFlagManager.SetFlag(flag.Key, !flag.Enabled, from.Name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Toggle block
|
||||
case >= 2000 and < 2000 + BlocksPerPage:
|
||||
{
|
||||
var index = buttonId - 2000;
|
||||
if (index < _displayedCount)
|
||||
{
|
||||
var block = _displayedBlocks[index];
|
||||
switch (_currentPage)
|
||||
{
|
||||
case FeatureFlagPage.GumpBlocks:
|
||||
{
|
||||
FeatureFlagManager.SetGumpBlockActive(block.ResolvedType, !block.Active, from.Name);
|
||||
break;
|
||||
}
|
||||
case FeatureFlagPage.ItemBlocks:
|
||||
{
|
||||
FeatureFlagManager.SetItemBlockActive(block.ResolvedType, !block.Active, from.Name);
|
||||
break;
|
||||
}
|
||||
case FeatureFlagPage.SkillBlocks:
|
||||
{
|
||||
FeatureFlagManager.SetSkillBlockActive(((SkillBlockEntry)block).Skill, !block.Active, from.Name);
|
||||
break;
|
||||
}
|
||||
case FeatureFlagPage.SpellBlocks:
|
||||
{
|
||||
FeatureFlagManager.SetSpellBlockActive(((SpellBlockEntry)block).SpellId, !block.Active, from.Name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// Remove block
|
||||
case >= 3000 and < 3000 + BlocksPerPage:
|
||||
{
|
||||
var index = buttonId - 3000;
|
||||
if (index < _displayedCount)
|
||||
{
|
||||
var block = _displayedBlocks[index];
|
||||
switch (_currentPage)
|
||||
{
|
||||
case FeatureFlagPage.GumpBlocks:
|
||||
{
|
||||
FeatureFlagManager.UnblockGump(block.ResolvedType, from.Name);
|
||||
break;
|
||||
}
|
||||
case FeatureFlagPage.ItemBlocks:
|
||||
{
|
||||
FeatureFlagManager.RemoveItemBlock(block.ResolvedType, from.Name);
|
||||
break;
|
||||
}
|
||||
case FeatureFlagPage.SkillBlocks:
|
||||
{
|
||||
FeatureFlagManager.UnblockSkill(((SkillBlockEntry)block).Skill, from.Name);
|
||||
break;
|
||||
}
|
||||
case FeatureFlagPage.SpellBlocks:
|
||||
{
|
||||
FeatureFlagManager.UnblockSpell(block.ResolvedType, from.Name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Edit item block (PropertiesGump)
|
||||
case >= 4000 and < 4000 + BlocksPerPage:
|
||||
{
|
||||
var index = buttonId - 4000;
|
||||
if (index < _displayedCount && _currentPage == FeatureFlagPage.ItemBlocks)
|
||||
{
|
||||
from.SendGump(new PropertiesGump(from, _displayedBlocks[index]));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
from.SendGump(this);
|
||||
}
|
||||
}
|
||||
602
Projects/UOContent/Engines/FeatureFlags/FeatureFlagCommands.cs
Normal file
602
Projects/UOContent/Engines/FeatureFlags/FeatureFlagCommands.cs
Normal file
|
|
@ -0,0 +1,602 @@
|
|||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1038
Projects/UOContent/Engines/FeatureFlags/FeatureFlagManager.cs
Normal file
1038
Projects/UOContent/Engines/FeatureFlags/FeatureFlagManager.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,82 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Server.Systems.FeatureFlags;
|
||||
|
||||
public sealed class FeatureFlag
|
||||
{
|
||||
public string Key { get; init; }
|
||||
public string Description { get; init; }
|
||||
public bool Enabled { get; set; }
|
||||
public bool DefaultEnabled { get; init; }
|
||||
public string Category { get; init; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public string LastModifiedBy { get; set; }
|
||||
}
|
||||
|
||||
[PropertyObject]
|
||||
public class FeatureFlagBlockEntry
|
||||
{
|
||||
[CommandProperty(AccessLevel.Administrator)]
|
||||
public Type ResolvedType { get; set; }
|
||||
|
||||
[CommandProperty(AccessLevel.Administrator)]
|
||||
public string Reason { get; set; }
|
||||
|
||||
[CommandProperty(AccessLevel.Administrator)]
|
||||
public bool Active { get; set; }
|
||||
|
||||
[CommandProperty(AccessLevel.Administrator, readOnly: true)]
|
||||
public DateTime CreatedAt { get; init; }
|
||||
|
||||
[CommandProperty(AccessLevel.Administrator, readOnly: true)]
|
||||
public string CreatedBy { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
public virtual string DisplayName => ResolvedType?.Name;
|
||||
}
|
||||
|
||||
public sealed class GumpBlockEntry : FeatureFlagBlockEntry;
|
||||
|
||||
public sealed class ItemBlockEntry : FeatureFlagBlockEntry
|
||||
{
|
||||
[CommandProperty(AccessLevel.Administrator)]
|
||||
public bool BlockUse { get; set; }
|
||||
|
||||
[CommandProperty(AccessLevel.Administrator)]
|
||||
public bool BlockEquip { get; set; }
|
||||
|
||||
[CommandProperty(AccessLevel.Administrator)]
|
||||
public bool BlockContainerAccess { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SkillBlockEntry : FeatureFlagBlockEntry
|
||||
{
|
||||
public SkillName Skill { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override string DisplayName => Skill.ToString();
|
||||
}
|
||||
|
||||
public sealed class SpellBlockEntry : FeatureFlagBlockEntry
|
||||
{
|
||||
[JsonIgnore]
|
||||
public int SpellId { get; set; }
|
||||
}
|
||||
|
||||
public static class FeatureFlagSettings
|
||||
{
|
||||
public const string DefaultGumpBlockedMessage = "This feature is temporarily disabled.";
|
||||
public const string DefaultItemUseBlockedMessage = "This item cannot be used at this time.";
|
||||
public const string DefaultItemEquipBlockedMessage = "This item cannot be equipped at this time.";
|
||||
public const string DefaultContainerBlockedMessage = "This container cannot be opened at this time.";
|
||||
public const string DefaultSkillDisabledMessage = "This skill is temporarily disabled.";
|
||||
public const string DefaultSpellDisabledMessage = "This spell is temporarily disabled.";
|
||||
|
||||
public static AccessLevel RequiredAccessLevel { get; set; } = AccessLevel.Administrator;
|
||||
public static bool LogChanges { get; set; } = true;
|
||||
public static bool BroadcastChangesToStaff { get; set; } = true;
|
||||
|
||||
public static string SavePath => Path.Combine(Core.BaseDirectory, "Configuration", "FeatureFlags");
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ public class GumpTextColors
|
|||
public const string BrightRed = "#FF0000";
|
||||
public const string LightGreen = "#E6FFC0";
|
||||
public const string Yellow = "#FFFFBB";
|
||||
public const string LightGray = "#CCCCCC";
|
||||
}
|
||||
|
||||
public class GumpHues
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
*************************************************************************/
|
||||
|
||||
using Server.Network;
|
||||
using Server.Systems.FeatureFlags;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
|
@ -105,6 +106,14 @@ public static partial class GumpSystem
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(m);
|
||||
|
||||
if (m.AccessLevel < FeatureFlagSettings.RequiredAccessLevel
|
||||
&& FeatureFlagManager.IsGumpBlocked(g.GetType()))
|
||||
{
|
||||
var entry = FeatureFlagManager.GetGumpBlockEntry(g.GetType());
|
||||
m.SendMessage(0x22, entry?.Reason ?? FeatureFlagSettings.DefaultGumpBlockedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
var state = m.NetState;
|
||||
|
||||
if (state != null)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Server.Collections;
|
|||
using Server.ContextMenus;
|
||||
using Server.Mobiles;
|
||||
using Server.Multis;
|
||||
using Server.Systems.FeatureFlags;
|
||||
|
||||
namespace Server.Items;
|
||||
|
||||
|
|
@ -19,6 +20,18 @@ public abstract class BaseContainer : Container
|
|||
|
||||
public override int DefaultMaxWeight => IsSecure ? 0 : base.DefaultMaxWeight;
|
||||
|
||||
public override void DisplayTo(Mobile to)
|
||||
{
|
||||
if (to.AccessLevel < FeatureFlagSettings.RequiredAccessLevel
|
||||
&& FeatureFlagManager.IsItemUseBlocked(GetType(), out var reason))
|
||||
{
|
||||
to.SendMessage(0x22, reason);
|
||||
return;
|
||||
}
|
||||
|
||||
base.DisplayTo(to);
|
||||
}
|
||||
|
||||
public override bool IsAccessibleTo(Mobile m) => BaseHouse.CheckAccessible(m, this) && base.IsAccessibleTo(m);
|
||||
|
||||
public override bool CheckHold(Mobile m, Item item, bool message, bool checkItems, int plusItems, int plusWeight)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ using ModernUO.Serialization;
|
|||
using Server.Commands;
|
||||
using Server.Engines.Craft;
|
||||
using Server.Ethics;
|
||||
using Server.Mobiles;
|
||||
using Server.Multis;
|
||||
using Server.Network;
|
||||
using Server.Spells;
|
||||
using Server.Systems.FeatureFlags;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Items;
|
||||
|
|
@ -306,6 +308,14 @@ public partial class Spellbook : Item, ICraftable, ISlayer, IAosItem
|
|||
return; // They are customizing
|
||||
}
|
||||
|
||||
// Early rejection by spell ID before instantiation
|
||||
if (from is PlayerMobile { AccessLevel: < AccessLevel.Administrator }
|
||||
&& FeatureFlagManager.IsSpellBlocked(spellID, out var reason))
|
||||
{
|
||||
from.SendMessage(0x22, reason);
|
||||
return;
|
||||
}
|
||||
|
||||
var book = item as Spellbook;
|
||||
|
||||
if (book?.HasSpell(spellID) != true)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ using Server.Spells.Necromancy;
|
|||
using Server.Spells.Ninjitsu;
|
||||
using Server.Spells.Sixth;
|
||||
using Server.Spells.Spellweaving;
|
||||
using Server.Systems.FeatureFlags;
|
||||
using Server.Targeting;
|
||||
using BaseQuestGump = Server.Engines.MLQuests.Gumps.BaseQuestGump;
|
||||
using CalcMoves = Server.Movement.Movement;
|
||||
|
|
@ -1764,16 +1765,25 @@ namespace Server.Mobiles
|
|||
|
||||
public override bool AllowItemUse(Item item)
|
||||
{
|
||||
if (DuelContext?.AllowItemUse(this, item) == false)
|
||||
if (AccessLevel < FeatureFlagSettings.RequiredAccessLevel &&
|
||||
FeatureFlagManager.IsItemUseBlocked(item.GetType(), out var reason))
|
||||
{
|
||||
SendMessage(0x22, reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
return DesignContext.Check(this);
|
||||
return DuelContext?.AllowItemUse(this, item) != false && DesignContext.Check(this);
|
||||
}
|
||||
|
||||
public override bool AllowSkillUse(SkillName skill)
|
||||
{
|
||||
if (AccessLevel < FeatureFlagSettings.RequiredAccessLevel
|
||||
&& FeatureFlagManager.IsSkillBlocked(skill, out var reason))
|
||||
{
|
||||
SendMessage(0x22, reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AnimalForm.UnderTransformation(this))
|
||||
{
|
||||
for (var i = 0; i < AnimalFormRestrictedSkills.Length; i++)
|
||||
|
|
@ -2033,6 +2043,13 @@ namespace Server.Mobiles
|
|||
|
||||
public override bool CheckEquip(Item item)
|
||||
{
|
||||
if (AccessLevel < FeatureFlagSettings.RequiredAccessLevel
|
||||
&& FeatureFlagManager.IsItemEquipBlocked(item.GetType(), out var reason))
|
||||
{
|
||||
SendMessage(0x22, reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!base.CheckEquip(item))
|
||||
{
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using Server.Mobiles;
|
|||
using Server.Network;
|
||||
using Server.Regions;
|
||||
using Server.Logging;
|
||||
using Server.Systems.FeatureFlags;
|
||||
|
||||
namespace Server.Mobiles
|
||||
{
|
||||
|
|
@ -851,6 +852,12 @@ namespace Server.Mobiles
|
|||
return;
|
||||
}
|
||||
|
||||
if (!ContentFeatureFlags.VendorPurchase && from.AccessLevel < AccessLevel.Administrator)
|
||||
{
|
||||
from.SendMessage(0x22, "Vendor purchases are temporarily disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CheckVendorAccess(from))
|
||||
{
|
||||
Say(501522); // I shall not treat with scum like thee!
|
||||
|
|
@ -1022,6 +1029,12 @@ namespace Server.Mobiles
|
|||
return;
|
||||
}
|
||||
|
||||
if (!ContentFeatureFlags.VendorSell && from.AccessLevel < AccessLevel.Administrator)
|
||||
{
|
||||
from.SendMessage(0x22, "Vendor sales are temporarily disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CheckVendorAccess(from))
|
||||
{
|
||||
Say(501522); // I shall not treat with scum like thee!
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using Server.Items;
|
|||
using Server.Misc;
|
||||
using Server.Multis;
|
||||
using Server.Prompts;
|
||||
using Server.Systems.FeatureFlags;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Mobiles;
|
||||
|
|
@ -730,6 +731,12 @@ public partial class PlayerVendor : Mobile
|
|||
return;
|
||||
}
|
||||
|
||||
if (!ContentFeatureFlags.PlayerVendors && from.AccessLevel < AccessLevel.Administrator)
|
||||
{
|
||||
from.SendMessage(0x22, "Player vendor transactions are temporarily disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (vendor.IsOwner(from))
|
||||
{
|
||||
vendor.SayTo(from, 503212); // You own this shop, just take what you want.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using ModernUO.Serialization;
|
||||
using Server.Engines.CannedEvil;
|
||||
using Server.Regions;
|
||||
using Server.Systems.FeatureFlags;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Multis;
|
||||
|
|
@ -66,64 +67,68 @@ public abstract partial class BaseBoatDeed : Item
|
|||
if (!IsChildOf(from.Backpack))
|
||||
{
|
||||
from.SendLocalizedMessage(1042001); // That must be in your pack for you to use it.
|
||||
return;
|
||||
}
|
||||
|
||||
var map = from.Map;
|
||||
|
||||
if (map == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ContentFeatureFlags.BoatPlacement && from.AccessLevel < FeatureFlagSettings.RequiredAccessLevel)
|
||||
{
|
||||
from.SendMessage(0x22, "Boat placement is temporarily disabled.");
|
||||
}
|
||||
|
||||
if (from.AccessLevel < AccessLevel.GameMaster && (map == Map.Ilshenar || map == Map.Malas))
|
||||
{
|
||||
from.SendLocalizedMessage(1043284); // A ship can not be created here.
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.Region.IsPartOf<HouseRegion>() || BaseBoat.FindBoatAt(from.Location, from.Map) != null)
|
||||
{
|
||||
// You may not place a ship while on another ship or inside a house.
|
||||
from.SendLocalizedMessage(1010568, null, 0x25);
|
||||
return;
|
||||
}
|
||||
|
||||
var boat = Boat;
|
||||
|
||||
if (boat == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
p = new Point3D(p.X - Offset.X, p.Y - Offset.Y, p.Z - Offset.Z);
|
||||
|
||||
if (BaseBoat.IsValidLocation(p, map) && boat.CanFit(p, map, boat.ItemID))
|
||||
{
|
||||
Delete();
|
||||
|
||||
boat.Owner = from;
|
||||
boat.Anchored = true;
|
||||
|
||||
var keyValue = boat.CreateKeys(from);
|
||||
|
||||
if (boat.PPlank != null)
|
||||
{
|
||||
boat.PPlank.KeyValue = keyValue;
|
||||
}
|
||||
|
||||
if (boat.SPlank != null)
|
||||
{
|
||||
boat.SPlank.KeyValue = keyValue;
|
||||
}
|
||||
|
||||
boat.MoveToWorld(p, map);
|
||||
}
|
||||
else
|
||||
{
|
||||
var map = from.Map;
|
||||
|
||||
if (map == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.AccessLevel < AccessLevel.GameMaster && (map == Map.Ilshenar || map == Map.Malas))
|
||||
{
|
||||
from.SendLocalizedMessage(1043284); // A ship can not be created here.
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.Region.IsPartOf<HouseRegion>() || BaseBoat.FindBoatAt(from.Location, from.Map) != null)
|
||||
{
|
||||
// You may not place a ship while on another ship or inside a house.
|
||||
from.SendLocalizedMessage(1010568, null, 0x25);
|
||||
return;
|
||||
}
|
||||
|
||||
var boat = Boat;
|
||||
|
||||
if (boat == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
p = new Point3D(p.X - Offset.X, p.Y - Offset.Y, p.Z - Offset.Z);
|
||||
|
||||
if (BaseBoat.IsValidLocation(p, map) && boat.CanFit(p, map, boat.ItemID))
|
||||
{
|
||||
Delete();
|
||||
|
||||
boat.Owner = from;
|
||||
boat.Anchored = true;
|
||||
|
||||
var keyValue = boat.CreateKeys(from);
|
||||
|
||||
if (boat.PPlank != null)
|
||||
{
|
||||
boat.PPlank.KeyValue = keyValue;
|
||||
}
|
||||
|
||||
if (boat.SPlank != null)
|
||||
{
|
||||
boat.SPlank.KeyValue = keyValue;
|
||||
}
|
||||
|
||||
boat.MoveToWorld(p, map);
|
||||
}
|
||||
else
|
||||
{
|
||||
boat.Delete();
|
||||
from.SendLocalizedMessage(1043284); // A ship can not be created here.
|
||||
}
|
||||
boat.Delete();
|
||||
from.SendLocalizedMessage(1043284); // A ship can not be created here.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using ModernUO.Serialization;
|
||||
using Server.Engines.CannedEvil;
|
||||
using Server.Regions;
|
||||
using Server.Systems.FeatureFlags;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Multis;
|
||||
|
|
@ -83,53 +84,58 @@ public abstract partial class BaseDockedBoat : Item
|
|||
if (!IsChildOf(from.Backpack))
|
||||
{
|
||||
from.SendLocalizedMessage(1042001); // That must be in your pack for you to use it.
|
||||
return;
|
||||
}
|
||||
|
||||
var map = from.Map;
|
||||
|
||||
if (map == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var boat = Boat;
|
||||
|
||||
if (boat == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ContentFeatureFlags.BoatPlacement && from.AccessLevel < FeatureFlagSettings.RequiredAccessLevel)
|
||||
{
|
||||
from.SendMessage(0x22, "Boat placement is temporarily disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
p = new Point3D(p.X - Offset.X, p.Y - Offset.Y, p.Z - Offset.Z);
|
||||
|
||||
if (BaseBoat.IsValidLocation(p, map) && boat.CanFit(p, map, boat.ItemID) && map != Map.Ilshenar &&
|
||||
map != Map.Malas)
|
||||
{
|
||||
Delete();
|
||||
|
||||
boat.Owner = from;
|
||||
boat.Anchored = true;
|
||||
boat.ShipName = _shipName;
|
||||
|
||||
var keyValue = boat.CreateKeys(from);
|
||||
|
||||
if (boat.PPlank != null)
|
||||
{
|
||||
boat.PPlank.KeyValue = keyValue;
|
||||
}
|
||||
|
||||
if (boat.SPlank != null)
|
||||
{
|
||||
boat.SPlank.KeyValue = keyValue;
|
||||
}
|
||||
|
||||
boat.MoveToWorld(p, map);
|
||||
}
|
||||
else
|
||||
{
|
||||
var map = from.Map;
|
||||
|
||||
if (map == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var boat = Boat;
|
||||
|
||||
if (boat == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
p = new Point3D(p.X - Offset.X, p.Y - Offset.Y, p.Z - Offset.Z);
|
||||
|
||||
if (BaseBoat.IsValidLocation(p, map) && boat.CanFit(p, map, boat.ItemID) && map != Map.Ilshenar &&
|
||||
map != Map.Malas)
|
||||
{
|
||||
Delete();
|
||||
|
||||
boat.Owner = from;
|
||||
boat.Anchored = true;
|
||||
boat.ShipName = _shipName;
|
||||
|
||||
var keyValue = boat.CreateKeys(from);
|
||||
|
||||
if (boat.PPlank != null)
|
||||
{
|
||||
boat.PPlank.KeyValue = keyValue;
|
||||
}
|
||||
|
||||
if (boat.SPlank != null)
|
||||
{
|
||||
boat.SPlank.KeyValue = keyValue;
|
||||
}
|
||||
|
||||
boat.MoveToWorld(p, map);
|
||||
}
|
||||
else
|
||||
{
|
||||
boat.Delete();
|
||||
from.SendLocalizedMessage(1043284); // A ship can not be created here.
|
||||
}
|
||||
boat.Delete();
|
||||
from.SendLocalizedMessage(1043284); // A ship can not be created here.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using Server.Collections;
|
||||
using Server.Regions;
|
||||
using Server.Spells;
|
||||
using Server.Systems.FeatureFlags;
|
||||
|
||||
namespace Server.Multis
|
||||
{
|
||||
|
|
@ -52,6 +53,11 @@ namespace Server.Multis
|
|||
return HousePlacementResult.BadLand; // A house cannot go here
|
||||
}
|
||||
|
||||
if (!ContentFeatureFlags.HousePlacement && from.AccessLevel < FeatureFlagSettings.RequiredAccessLevel)
|
||||
{
|
||||
return HousePlacementResult.BadRegionTemp;
|
||||
}
|
||||
|
||||
if (from.AccessLevel >= AccessLevel.GameMaster)
|
||||
{
|
||||
return HousePlacementResult.Valid; // Staff can place anywhere
|
||||
|
|
|
|||
|
|
@ -21,14 +21,13 @@ public partial class HousePlacementTool : Item
|
|||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (IsChildOf(from.Backpack))
|
||||
{
|
||||
from.SendGump(new HousePlacementCategoryGump());
|
||||
}
|
||||
else
|
||||
if (!IsChildOf(from.Backpack))
|
||||
{
|
||||
from.SendLocalizedMessage(1042001); // That must be in your pack for you to use it.
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendGump(new HousePlacementCategoryGump());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using Server.Spells.Necromancy;
|
|||
using Server.Spells.Ninjitsu;
|
||||
using Server.Spells.Second;
|
||||
using Server.Spells.Spellweaving;
|
||||
using Server.Systems.FeatureFlags;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Spells
|
||||
|
|
@ -508,6 +509,12 @@ namespace Server.Spells
|
|||
else if ((Caster as PlayerMobile)?.DuelContext?.AllowSpellCast(Caster, this) == false)
|
||||
{
|
||||
}
|
||||
else if (Caster is PlayerMobile { AccessLevel: < AccessLevel.Administrator } &&
|
||||
FeatureFlagManager.IsSpellBlocked(GetType()))
|
||||
{
|
||||
var entry = FeatureFlagManager.GetSpellBlockEntry(GetType());
|
||||
Caster.SendMessage(0x22, entry?.Reason ?? "This spell is temporarily disabled.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var requiredMana = ScaleMana(GetMana());
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ namespace Server.Spells
|
|||
|
||||
public static int GetRegistryNumber(SpecialMove s) => GetRegistryNumber(s.GetType());
|
||||
|
||||
public static int GetRegistryNumber(Type type) => m_IDsFromTypes.TryGetValue(type, out var value) ? value : -1;
|
||||
public static int GetRegistryNumber(Type type) => m_IDsFromTypes.GetValueOrDefault(type, -1);
|
||||
|
||||
public static void Register(int spellID, Type type)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue