## 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
169 lines
5.4 KiB
C#
169 lines
5.4 KiB
C#
using ModernUO.Serialization;
|
|
using Server.Gumps;
|
|
using Server.Mobiles;
|
|
using Server.Systems.FeatureFlags;
|
|
|
|
namespace Server.Engines.BulkOrders
|
|
{
|
|
[SerializationGenerator(1)]
|
|
public abstract partial class LargeBOD : BaseBOD
|
|
{
|
|
[InvalidateProperties]
|
|
[SerializableField(0)]
|
|
private LargeBulkEntry[] _entries;
|
|
|
|
public LargeBOD(
|
|
int hue, int amountMax, bool requireExeptional, BulkMaterialType material, LargeBulkEntry[] entries
|
|
) : base(hue, amountMax, requireExeptional, material) => _entries = entries;
|
|
|
|
public LargeBOD()
|
|
{
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public override bool Complete
|
|
{
|
|
get
|
|
{
|
|
for (var i = 0; i < _entries.Length; ++i)
|
|
{
|
|
if (_entries[i].Amount < AmountMax)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public override int LabelNumber => 1045151; // a bulk order deed
|
|
|
|
public override void GetProperties(IPropertyList list)
|
|
{
|
|
base.GetProperties(list);
|
|
|
|
list.Add(1060655); // large bulk order
|
|
|
|
if (RequireExceptional)
|
|
{
|
|
list.Add(1045141); // All items must be exceptional.
|
|
}
|
|
|
|
if (Material != BulkMaterialType.None)
|
|
{
|
|
list.Add(LargeBODGump.GetMaterialNumberFor(Material)); // All items must be made with x material.
|
|
}
|
|
|
|
list.Add(1060656, AmountMax); // amount to make: ~1_val~
|
|
|
|
for (var i = 0; i < _entries.Length; ++i)
|
|
{
|
|
var entry = _entries[i];
|
|
list.Add(1060658 + i, $"{entry.Details.Number:#}\t{entry.Amount}"); // ~1_val~: ~2_val~
|
|
}
|
|
}
|
|
|
|
public override void OnDoubleClickNotAccessible(Mobile from)
|
|
{
|
|
OnDoubleClick(from);
|
|
}
|
|
|
|
public override void OnDoubleClickSecureTrade(Mobile from)
|
|
{
|
|
OnDoubleClick(from);
|
|
}
|
|
|
|
public override void OnDoubleClick(Mobile from)
|
|
{
|
|
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)
|
|
{
|
|
if (item is not SmallBOD small)
|
|
{
|
|
from.SendLocalizedMessage(1045159); // That is not a bulk order.
|
|
return;
|
|
}
|
|
|
|
LargeBulkEntry entry = null;
|
|
|
|
for (var i = 0; i < _entries.Length; ++i)
|
|
{
|
|
if (_entries[i].Details.Type == small.Type)
|
|
{
|
|
entry = _entries[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (entry == null)
|
|
{
|
|
from.SendLocalizedMessage(1045160); // That is not a bulk order for this large request.
|
|
}
|
|
else if (RequireExceptional && !small.RequireExceptional)
|
|
{
|
|
from.SendLocalizedMessage(1045161); // Both orders must be of exceptional quality.
|
|
}
|
|
else if (Material >= BulkMaterialType.DullCopper && Material <= BulkMaterialType.Valorite &&
|
|
small.Material != Material)
|
|
{
|
|
from.SendLocalizedMessage(1045162); // Both orders must use the same ore type.
|
|
}
|
|
else if (Material >= BulkMaterialType.Spined && Material <= BulkMaterialType.Barbed &&
|
|
small.Material != Material)
|
|
{
|
|
from.SendLocalizedMessage(1049351); // Both orders must use the same leather type.
|
|
}
|
|
else if (AmountMax != small.AmountMax)
|
|
{
|
|
// The two orders have different requested amounts and cannot be combined.
|
|
from.SendLocalizedMessage(1045163);
|
|
}
|
|
else if (small.AmountCur < small.AmountMax)
|
|
{
|
|
from.SendLocalizedMessage(1045164); // The order to combine with is not completed.
|
|
}
|
|
else if (entry.Amount >= AmountMax)
|
|
{
|
|
// The maximum amount of requested items have already been combined to this deed.
|
|
from.SendLocalizedMessage(1045166);
|
|
}
|
|
else
|
|
{
|
|
entry.Amount += small.AmountCur;
|
|
small.Delete();
|
|
|
|
from.SendLocalizedMessage(1045165); // The orders have been combined.
|
|
from.SendGump(new LargeBODGump(this));
|
|
|
|
if (!Complete)
|
|
{
|
|
BeginCombine(from);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Deserialize(IGenericReader reader, int version)
|
|
{
|
|
_entries = new LargeBulkEntry[reader.ReadInt()];
|
|
for (var i = 0; i < _entries.Length; i++)
|
|
{
|
|
_entries[i] = new LargeBulkEntry(reader, this);
|
|
}
|
|
}
|
|
}
|
|
}
|