## Summary
- **Add a self-referencing `InterpolationHandler` to `ValueStringBuilder`** that writes directly into the builder's buffer — zero intermediate allocation, works with `stackalloc`-backed builders
- **Replace all `System.Text.StringBuilder` usage** across the codebase with `ValueStringBuilder`
- **Convert `ValueStringBuilder.Create()` to `stackalloc`** at 10 sites where output length is provably bounded
- **Convert manual `Dispose()` to `using var`** where possible, and hoist loop-scoped builders outside loops with `Reset()`
- **Convert verbose `Append()` chains to `Append($"...")`** interpolation for readability
- **Add comprehensive documentation** for string handling patterns
## InterpolationHandler Design
`ValueStringBuilder` is a `ref struct`, which creates challenges for C#'s interpolated string handler pattern:
- **`ref` fields to ref structs are not allowed** (CS9050)
- **`[InterpolatedStringHandlerArgument("")]` passes struct receivers by value**, not by ref
- **`ISelfInterpolatedStringHandler` requires boxing** ref structs into interface fields
**Solution: Copy-and-reconcile pattern.** The handler receives a value copy of the builder. The copy shares the same underlying `char` buffer (`Span` points to the same `stackalloc`/pooled memory), so writes go to the original buffer. `Append()` reconciles by `this = handler._builder`, updating `_length` and any buffer references changed by `Grow()`.
This is safe because:
- The game loop is single-threaded — no concurrent access between handler construction and reconciliation
- If `Grow()` occurs in the copy, the original's stale buffer isn't accessed until `Append()` replaces it
- `Dispose()` correctly returns the reconciled buffer to the pool
## Changes by Category
### ValueStringBuilder (`Projects/Server/Buffers/ValueStringBuilder.cs`)
- Added nested `InterpolationHandler` ref struct with copy-and-reconcile pattern
- Added `Append([InterpolatedStringHandlerArgument("")] scoped ref InterpolationHandler)` method
- Removed `RawInterpolatedStringHandler` overloads (new handler replaces them)
- All `AppendFormatted` overloads delegate to existing `Append` methods (no code duplication)
- Alignment support via direct private field access (nested type privilege)
### StringBuilder → ValueStringBuilder (15 files)
Replaced all `new StringBuilder()` with `ValueStringBuilder.Create()` or `stackalloc`:
- ConPVP games: KingOfTheHill, DoubleDom, CTF, BombingRun, TourneyMatch
- ConPVP infrastructure: Tournament, Participant, TourneyParticipant
- ConPVP gumps: ArenaGump, TournamentBracketGump, AcceptTeamGump, ConfirmSignupGump
- Commands: Handlers, Logging, Add
- Other: TownCrier, SpeechLogGump, TestCenter
Key patterns:
- `sb = new StringBuilder()` reassignment → `sb.Reset()`
- `sb.AppendFormat("{0:N0}", value)` → `sb.Append($"{value:N0}")`
- `sb.Append(x).Append(y)` chains → separate statements (VSB returns void)
### Create() → stackalloc (10 files)
Converted heap-allocated builders to stackalloc where output is bounded:
- ClientVersion (32), MapSelection (160), HouseRaffleStone (48)
- HolySense (96), UnholySense (96), ClientVerification (192)
- AcceptTeamGump (64), ConfirmSignupGump (64)
- BaseWeapon (160), BaseArmor (128)
### Loop optimizations (2 files)
Hoisted `ValueStringBuilder` creation outside loops with `Reset()` per iteration:
- TourneyMatch.cs: `using var` inside for loop → stackalloc before loop
- ArenaGump.cs: `Create()` + `Dispose()` per iteration → stackalloc before loop
### Append chain → interpolation (5 files)
Converted multi-line `Append()` chains to `Append($"...")`:
- BountyMessage.cs: title switch (6 cases), paragraph (15→1 Append), description lines, closing
- AcceptTeamGump, ConfirmSignupGump, TournamentBracketGump: tournament type strings
- AdminGump: comment/tag formatting in loops
### Documentation
- `dev-docs/string-handling.md`: Full reference — construction, interpolation, disposal, decision guide
- `dev-docs/claude-skills/modernuo-string-handling.md`: Claude skill with quick reference
- `CLAUDE.md`: Added rule 17 (no StringBuilder), dev-docs table entry, skills table entry
- `dev-docs/code-standards.md`: Updated memory management section
## Test Plan
- [x] `dotnet build` — 0 errors, 0 warnings
- [x] `dotnet test` — 940/940 tests pass
- [x] 28 ValueStringBuilder tests covering all reconciliation scenarios:
- Stackalloc no-grow, stackalloc with grow (→pool transition)
- Heap no-grow, heap with grow, heap double grow
- Pre-existing content with and without grow
- Sequential multiple `Append($"...")` calls
- Mixed plain + interpolated Append
- Empty interpolation, literal-only, format specifiers
- Null string holes, ISpanFormattable types
- Dispose after stackalloc→pool grow
933 lines
32 KiB
C#
933 lines
32 KiB
C#
using System.Collections.Generic;
|
|
using Server.Commands.Generic;
|
|
using Server.Text;
|
|
using Server.Engines.Help;
|
|
using Server.Gumps;
|
|
using Server.Items;
|
|
using Server.Menus.ItemLists;
|
|
using Server.Menus.Questions;
|
|
using Server.Mobiles;
|
|
using Server.Multis;
|
|
using Server.Network;
|
|
using Server.Spells;
|
|
using Server.Targeting;
|
|
using Server.Targets;
|
|
|
|
namespace Server.Commands
|
|
{
|
|
public static class CommandHandlers
|
|
{
|
|
public static void Configure()
|
|
{
|
|
CommandSystem.Prefix = ServerConfiguration.GetOrUpdateSetting("commandsystem.prefix", "[");
|
|
|
|
Register("Go", AccessLevel.Counselor, Go_OnCommand);
|
|
Register("DropHolding", AccessLevel.Counselor, DropHolding_OnCommand);
|
|
Register("GetFollowers", AccessLevel.GameMaster, GetFollowers_OnCommand);
|
|
Register("Where", AccessLevel.Counselor, Where_OnCommand);
|
|
Register("AutoPageNotify", AccessLevel.Counselor, APN_OnCommand);
|
|
Register("Animate", AccessLevel.GameMaster, Animate_OnCommand);
|
|
Register("Cast", AccessLevel.Counselor, Cast_OnCommand);
|
|
Register("Stuck", AccessLevel.Counselor, Stuck_OnCommand);
|
|
Register("Help", AccessLevel.Player, Help_OnCommand);
|
|
Register("Move", AccessLevel.GameMaster, Move_OnCommand);
|
|
Register("Client", AccessLevel.Counselor, Client_OnCommand);
|
|
Register("SMsg", AccessLevel.Counselor, StaffMessage_OnCommand);
|
|
Register("BCast", AccessLevel.GameMaster, BroadcastMessage_OnCommand);
|
|
Register("Bank", AccessLevel.GameMaster, Bank_OnCommand);
|
|
Register("Echo", AccessLevel.Counselor, Echo_OnCommand);
|
|
Register("Sound", AccessLevel.GameMaster, Sound_OnCommand);
|
|
Register("ViewEquip", AccessLevel.GameMaster, ViewEquip_OnCommand);
|
|
Register("Light", AccessLevel.Counselor, Light_OnCommand);
|
|
Register("Stats", AccessLevel.Counselor, Stats_OnCommand);
|
|
Register("SpeedBoost", AccessLevel.Counselor, SpeedBoost_OnCommand);
|
|
}
|
|
|
|
public static void Register(string command, AccessLevel access, CommandEventHandler handler)
|
|
{
|
|
CommandSystem.Register(command, access, handler);
|
|
}
|
|
|
|
[Usage("SpeedBoost [true|false]")]
|
|
[Description("Enables a speed boost for the invoker. Disable with parameters.")]
|
|
private static void SpeedBoost_OnCommand(CommandEventArgs e)
|
|
{
|
|
var from = e.Mobile;
|
|
|
|
if (e.Length <= 1)
|
|
{
|
|
if (e.Length == 1 && !e.GetBoolean(0))
|
|
{
|
|
from.NetState.SendSpeedControl(SpeedControlSetting.Disable);
|
|
from.SendMessage("Speed boost has been disabled.");
|
|
}
|
|
else
|
|
{
|
|
from.NetState.SendSpeedControl(SpeedControlSetting.Mount);
|
|
from.SendMessage("Speed boost has been enabled.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("Format: SpeedBoost [true|false]");
|
|
}
|
|
}
|
|
|
|
[Usage("Where")]
|
|
[Description("Tells the commanding player his coordinates, region, and facet.")]
|
|
public static void Where_OnCommand(CommandEventArgs e)
|
|
{
|
|
var from = e.Mobile;
|
|
var map = from.Map;
|
|
|
|
from.SendMessage($"You are at {from.X} {from.Y} {from.Z} in {map}.");
|
|
|
|
if (map != null)
|
|
{
|
|
var reg = Region.Find(from.Location, from.Map);
|
|
|
|
if (!reg.IsDefault)
|
|
{
|
|
using var builder = ValueStringBuilder.Create(256);
|
|
|
|
builder.Append(reg);
|
|
reg = reg.Parent;
|
|
|
|
while (reg != null)
|
|
{
|
|
builder.Append($" <- {reg}");
|
|
reg = reg.Parent;
|
|
}
|
|
|
|
from.SendMessage($"Your region is {builder.ToString()}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Usage("DropHolding")]
|
|
[Description("Drops the item, if any, that a targeted player is holding. The item is placed into their backpack, or if that's full, at their feet.")]
|
|
public static void DropHolding_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.BeginTarget(-1, false, TargetFlags.None, DropHolding_OnTarget);
|
|
e.Mobile.SendMessage("Target the player to drop what they are holding.");
|
|
}
|
|
|
|
public static void DropHolding_OnTarget(Mobile from, object obj)
|
|
{
|
|
if (obj is Mobile targ && targ.Player)
|
|
{
|
|
var held = targ.Holding;
|
|
|
|
if (held == null)
|
|
{
|
|
from.SendMessage("They are not holding anything.");
|
|
}
|
|
else
|
|
{
|
|
if (from.AccessLevel == AccessLevel.Counselor)
|
|
{
|
|
var pe = PageQueue.GetEntry(targ);
|
|
|
|
if (pe?.Handler == from)
|
|
{
|
|
from.SendMessage("You may only use this command if you are handling their help page.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("You may only use this command on someone who has paged you.");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (targ.AddToBackpack(held))
|
|
{
|
|
from.SendMessage("The item they were holding has been placed into their backpack.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("The item they were holding has been placed at their feet.");
|
|
}
|
|
|
|
held.ClearBounce();
|
|
|
|
targ.Holding = null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.BeginTarget(-1, false, TargetFlags.None, DropHolding_OnTarget);
|
|
from.SendMessage("That is not a player. Try again.");
|
|
}
|
|
}
|
|
|
|
[Usage("GetFollowers")]
|
|
[Description("Teleports all pets of a targeted player to your location.")]
|
|
public static void GetFollowers_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.BeginTarget(-1, false, TargetFlags.None, GetFollowers_OnTarget);
|
|
e.Mobile.SendMessage("Target a player to get their pets.");
|
|
}
|
|
|
|
public static void GetFollowers_OnTarget(Mobile from, object obj)
|
|
{
|
|
if (obj is PlayerMobile pm)
|
|
{
|
|
var pets = pm.AllFollowers;
|
|
|
|
if (!(pets?.Count > 0))
|
|
{
|
|
from.SendMessage("There were no pets found for that player.");
|
|
return;
|
|
}
|
|
|
|
CommandLogging.WriteLine(
|
|
from,
|
|
$"{from.AccessLevel} {CommandLogging.Format(from)} getting all followers of {CommandLogging.Format(pm)}"
|
|
);
|
|
|
|
if (pets.Count == 1)
|
|
{
|
|
from.SendMessage($"That player has {pets.Count} pet.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage($"That player has {pets.Count} pets.");
|
|
}
|
|
|
|
foreach (var pet in pets)
|
|
{
|
|
if (pet is IMount mount)
|
|
{
|
|
mount.Rider = null; // make sure it's dismounted
|
|
}
|
|
|
|
pet.MoveToWorld(from.Location, from.Map);
|
|
}
|
|
}
|
|
else if (obj is Mobile master && master.Player)
|
|
{
|
|
var pets = new List<BaseCreature>();
|
|
|
|
foreach (var m in World.Mobiles.Values)
|
|
{
|
|
if (m is BaseCreature bc)
|
|
{
|
|
if (bc.Controlled && bc.ControlMaster == master || bc.Summoned && bc.SummonMaster == master)
|
|
{
|
|
pets.Add(bc);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pets.Count > 0)
|
|
{
|
|
CommandLogging.WriteLine(
|
|
from,
|
|
$"{from.AccessLevel} {CommandLogging.Format(from)} getting all followers of {CommandLogging.Format(master)}"
|
|
);
|
|
|
|
if (pets.Count == 1)
|
|
{
|
|
from.SendMessage($"That player has {pets.Count} pet.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage($"That player has {pets.Count} pets.");
|
|
}
|
|
|
|
for (var i = 0; i < pets.Count; ++i)
|
|
{
|
|
var pet = pets[i];
|
|
|
|
if (pet is IMount mount)
|
|
{
|
|
mount.Rider = null; // make sure it's dismounted
|
|
}
|
|
|
|
pet.MoveToWorld(from.Location, from.Map);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("There were no pets found for that player.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.BeginTarget(-1, false, TargetFlags.None, GetFollowers_OnTarget);
|
|
from.SendMessage("That is not a player. Try again.");
|
|
}
|
|
}
|
|
|
|
[Usage("ViewEquip"),
|
|
Description("Lists equipment of a targeted mobile. From the list you can move, delete, or open props.")]
|
|
public static void ViewEquip_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.Target = new ViewEqTarget();
|
|
}
|
|
|
|
[Usage("Sound <index> [toAll=true]")]
|
|
[Description("Plays a sound to players within 12 tiles of you. The (toAll) argument specifies to everyone, or just those who can see you.")]
|
|
public static void Sound_OnCommand(CommandEventArgs e)
|
|
{
|
|
if (e.Length == 1)
|
|
{
|
|
PlaySound(e.Mobile, e.GetInt32(0), true);
|
|
}
|
|
else if (e.Length == 2)
|
|
{
|
|
PlaySound(e.Mobile, e.GetInt32(0), e.GetBoolean(1));
|
|
}
|
|
else
|
|
{
|
|
e.Mobile.SendMessage("Format: Sound <index> [toAll]");
|
|
}
|
|
}
|
|
|
|
private static void PlaySound(Mobile m, int index, bool toAll)
|
|
{
|
|
var map = m.Map;
|
|
|
|
if (map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CommandLogging.WriteLine(
|
|
m,
|
|
$"{m.AccessLevel} {CommandLogging.Format(m)} playing sound {index} (toAll={toAll})"
|
|
);
|
|
|
|
var buffer = stackalloc byte[OutgoingEffectPackets.SoundPacketLength].InitializePacket();
|
|
|
|
foreach (var state in m.GetClientsInRange(12))
|
|
{
|
|
if (toAll || state.Mobile.CanSee(m))
|
|
{
|
|
OutgoingEffectPackets.CreateSoundEffect(buffer, index, m);
|
|
state.Send(buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Usage("Echo <text>")]
|
|
[Description("Relays (text) as a system message.")]
|
|
public static void Echo_OnCommand(CommandEventArgs e)
|
|
{
|
|
var toEcho = e.ArgString.Trim();
|
|
|
|
if (toEcho.Length > 0)
|
|
{
|
|
e.Mobile.SendMessage(toEcho);
|
|
}
|
|
else
|
|
{
|
|
e.Mobile.SendMessage("Format: Echo \"<text>\"");
|
|
}
|
|
}
|
|
|
|
[Usage("Bank")]
|
|
[Description("Opens the bank box of a given target.")]
|
|
public static void Bank_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.Target = new BankTarget();
|
|
}
|
|
|
|
[Usage("Client")]
|
|
[Description("Opens the client gump menu for a given player.")]
|
|
private static void Client_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.Target = new ClientTarget();
|
|
}
|
|
|
|
[Usage("Move")]
|
|
[Description("Repositions a targeted item or mobile.")]
|
|
private static void Move_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.Target = new PickMoveTarget();
|
|
}
|
|
|
|
private static bool FixMap(ref Map map, ref Point3D loc, Item item) =>
|
|
map != null && map != Map.Internal || item.RootParent is Mobile m &&
|
|
FixMap(ref map, ref loc, m);
|
|
|
|
private static bool FixMap(ref Map map, ref Point3D loc, Mobile m)
|
|
{
|
|
var validMap = map != null && map != Map.Internal;
|
|
|
|
if (!validMap)
|
|
{
|
|
map = m.LogoutMap;
|
|
loc = m.LogoutLocation;
|
|
}
|
|
|
|
return validMap;
|
|
}
|
|
|
|
[Usage("Go [name | serial | (x y [z]) | (deg min (N | S) deg min (E | W))]")]
|
|
[Description("With no arguments, this command brings up the go menu. With one argument, (name), you are moved to that regions \"go location.\" Or, if a numerical value is specified for one argument, (serial), you are moved to that object. Two or three arguments, (x y [z]), will move your character to that location. When six arguments are specified, (deg min (N | S) deg min (E | W)), your character will go to an approximate of those sextant coordinates.")]
|
|
private static void Go_OnCommand(CommandEventArgs e)
|
|
{
|
|
var from = e.Mobile;
|
|
|
|
if (e.Length == 0)
|
|
{
|
|
GoGump.DisplayTo(from);
|
|
return;
|
|
}
|
|
|
|
if (e.Length == 1)
|
|
{
|
|
try
|
|
{
|
|
var ser = (Serial)e.GetUInt32(0);
|
|
|
|
var ent = World.FindEntity(ser);
|
|
|
|
if (ent is Item item)
|
|
{
|
|
var map = item.Map;
|
|
var loc = item.GetWorldLocation();
|
|
|
|
var owner = item.RootParent as Mobile;
|
|
|
|
if (owner?.Map != null && owner.Map != Map.Internal &&
|
|
!BaseCommand.IsAccessible(from, owner) /* !from.CanSee( owner )*/)
|
|
{
|
|
from.SendMessage("You can not go to what you can not see.");
|
|
return;
|
|
}
|
|
|
|
if (owner != null && (owner.Map == null || owner.Map == Map.Internal) && owner.Hidden &&
|
|
owner.AccessLevel >= from.AccessLevel)
|
|
{
|
|
from.SendMessage("You can not go to what you can not see.");
|
|
return;
|
|
}
|
|
|
|
if (!FixMap(ref map, ref loc, item))
|
|
{
|
|
from.SendMessage("That is an internal item and you cannot go to it.");
|
|
return;
|
|
}
|
|
|
|
from.MoveToWorld(loc, map);
|
|
|
|
return;
|
|
}
|
|
|
|
if (ent is Mobile m)
|
|
{
|
|
var map = m.Map;
|
|
var loc = m.Location;
|
|
|
|
var owner = m;
|
|
|
|
if (owner.Map != null && owner.Map != Map.Internal &&
|
|
!BaseCommand.IsAccessible(from, owner) /* !from.CanSee( owner )*/)
|
|
{
|
|
from.SendMessage("You can not go to what you can not see.");
|
|
return;
|
|
}
|
|
|
|
if ((owner.Map == null || owner.Map == Map.Internal) && owner.Hidden &&
|
|
owner.AccessLevel >= from.AccessLevel)
|
|
{
|
|
from.SendMessage("You can not go to what you can not see.");
|
|
return;
|
|
}
|
|
|
|
if (!FixMap(ref map, ref loc, m))
|
|
{
|
|
from.SendMessage("That is an internal mobile and you cannot go to it.");
|
|
return;
|
|
}
|
|
|
|
from.MoveToWorld(loc, map);
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
var name = e.GetString(0);
|
|
Map map;
|
|
|
|
for (var i = 0; i < Map.AllMaps.Count; ++i)
|
|
{
|
|
map = Map.AllMaps[i];
|
|
|
|
if (map.MapIndex is 0x7F or 0xFF)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (name.InsensitiveEquals(map.Name))
|
|
{
|
|
from.Map = map;
|
|
return;
|
|
}
|
|
}
|
|
|
|
var list = from.Map.Regions;
|
|
|
|
foreach (var kvp in list)
|
|
{
|
|
var r = kvp.Value;
|
|
|
|
if (r.Name.InsensitiveEquals(name))
|
|
{
|
|
from.Location = new Point3D(r.GoLocation);
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < Map.AllMaps.Count; ++i)
|
|
{
|
|
map = Map.AllMaps[i];
|
|
|
|
if (map.MapIndex is 0x7F or 0xFF || from.Map == map)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var r in map.Regions.Values)
|
|
{
|
|
if (r.Name.InsensitiveEquals(name))
|
|
{
|
|
from.MoveToWorld(r.GoLocation, map);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ser != 0)
|
|
{
|
|
from.SendMessage("No object with that serial was found.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("No region with that name was found.");
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
|
|
from.SendMessage("Region name not found");
|
|
}
|
|
else if (e.Length is 2 or 3)
|
|
{
|
|
var map = from.Map;
|
|
|
|
if (map != null)
|
|
{
|
|
try
|
|
{
|
|
/*
|
|
* This to avoid being teleported to (0,0) if trying to teleport
|
|
* to a region with spaces in its name.
|
|
*/
|
|
var x = int.Parse(e.GetString(0));
|
|
var y = int.Parse(e.GetString(1));
|
|
var z = e.Length == 3 ? int.Parse(e.GetString(2)) : map.GetAverageZ(x, y);
|
|
|
|
from.Location = new Point3D(x, y, z);
|
|
}
|
|
catch
|
|
{
|
|
from.SendMessage("Region name not found.");
|
|
}
|
|
}
|
|
}
|
|
else if (e.Length == 6)
|
|
{
|
|
var map = from.Map;
|
|
|
|
if (map != null)
|
|
{
|
|
var p = Sextant.ReverseLookup(
|
|
map,
|
|
e.GetInt32(3),
|
|
e.GetInt32(0),
|
|
e.GetInt32(4),
|
|
e.GetInt32(1),
|
|
e.GetString(5).InsensitiveEquals("E"),
|
|
e.GetString(2).InsensitiveEquals("S")
|
|
);
|
|
|
|
if (p != Point3D.Zero)
|
|
{
|
|
from.Location = p;
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("Sextant reverse lookup failed.");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("Format: Go [name | serial | (x y [z]) | (deg min (N | S) deg min (E | W)]");
|
|
}
|
|
}
|
|
|
|
[Usage("Help")]
|
|
[Description("Lists all available commands.")]
|
|
public static void Help_OnCommand(CommandEventArgs e)
|
|
{
|
|
var m = e.Mobile;
|
|
|
|
var list = new List<CommandEntry>();
|
|
|
|
foreach (var entry in CommandSystem.Entries.Values)
|
|
{
|
|
if (m.AccessLevel >= entry.AccessLevel)
|
|
{
|
|
list.Add(entry);
|
|
}
|
|
}
|
|
|
|
list.Sort();
|
|
|
|
using var sb = ValueStringBuilder.Create(256);
|
|
|
|
if (list.Count > 0)
|
|
{
|
|
sb.Append(list[0].Command);
|
|
}
|
|
|
|
for (var i = 1; i < list.Count; ++i)
|
|
{
|
|
var v = list[i].Command;
|
|
|
|
if (sb.Length + 1 + v.Length >= 256)
|
|
{
|
|
m.SendAsciiMessage(0x482, sb.ToString());
|
|
sb.Reset();
|
|
sb.Append(v);
|
|
}
|
|
else
|
|
{
|
|
sb.Append(' ');
|
|
sb.Append(v);
|
|
}
|
|
}
|
|
|
|
if (sb.Length > 0)
|
|
{
|
|
m.SendAsciiMessage(0x482, sb.ToString());
|
|
}
|
|
}
|
|
|
|
[Usage("SMsg <text>")]
|
|
[Aliases("S", "SM")]
|
|
[Description("Broadcasts a message to all online staff.")]
|
|
public static void StaffMessage_OnCommand(CommandEventArgs e)
|
|
{
|
|
BroadcastMessage(AccessLevel.Counselor, e.Mobile.SpeechHue, $"[{e.Mobile.Name}] {e.ArgString}");
|
|
}
|
|
|
|
[Usage("BCast <text>")]
|
|
[Aliases("B", "BC")]
|
|
[Description("Broadcasts a message to everyone online.")]
|
|
public static void BroadcastMessage_OnCommand(CommandEventArgs e)
|
|
{
|
|
BroadcastMessage(AccessLevel.Player, 0x482, $"Staff message from {e.Mobile.Name}:");
|
|
BroadcastMessage(AccessLevel.Player, 0x482, e.ArgString);
|
|
}
|
|
|
|
public static void BroadcastMessage(AccessLevel ac, int hue, string message)
|
|
{
|
|
foreach (var state in NetState.Instances)
|
|
{
|
|
var m = state.Mobile;
|
|
|
|
if (m?.AccessLevel >= ac)
|
|
{
|
|
m.SendMessage(hue, message);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Usage("AutoPageNotify")]
|
|
[Aliases("APN")]
|
|
[Description("Toggles your auto-page-notify status.")]
|
|
public static void APN_OnCommand(CommandEventArgs e)
|
|
{
|
|
var m = e.Mobile;
|
|
|
|
m.AutoPageNotify = !m.AutoPageNotify;
|
|
|
|
if (m.AutoPageNotify)
|
|
{
|
|
m.SendMessage("Your auto-page-notify has been turned on.");
|
|
}
|
|
else
|
|
{
|
|
m.SendMessage("Your auto-page-notify has been turned off.");
|
|
}
|
|
}
|
|
|
|
[Usage("Animate <action> <frameCount> <repeatCount> <forward> <repeat> <delay>")]
|
|
[Description("Makes your character do a specified animation.")]
|
|
public static void Animate_OnCommand(CommandEventArgs e)
|
|
{
|
|
if (e.Length == 6)
|
|
{
|
|
e.Mobile.Animate(
|
|
e.GetInt32(0),
|
|
e.GetInt32(1),
|
|
e.GetInt32(2),
|
|
e.GetBoolean(3),
|
|
e.GetBoolean(4),
|
|
e.GetInt32(5)
|
|
);
|
|
}
|
|
else
|
|
{
|
|
e.Mobile.SendMessage("Format: Animate <action> <frameCount> <repeatCount> <forward> <repeat> <delay>");
|
|
}
|
|
}
|
|
|
|
[Usage("Cast <name>")]
|
|
[Description("Casts a spell by name.")]
|
|
public static void Cast_OnCommand(CommandEventArgs e)
|
|
{
|
|
if (e.Length == 1)
|
|
{
|
|
if (!DesignContext.Check(e.Mobile))
|
|
{
|
|
return; // They are customizing
|
|
}
|
|
|
|
var spell = SpellRegistry.NewSpell(e.GetString(0), e.Mobile, null);
|
|
|
|
if (spell != null)
|
|
{
|
|
spell.Cast();
|
|
}
|
|
else
|
|
{
|
|
e.Mobile.SendMessage("That spell was not found.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
e.Mobile.SendMessage("Format: Cast <name>");
|
|
}
|
|
}
|
|
|
|
[Usage("Stuck")]
|
|
[Description("Opens a menu of towns, used for teleporting stuck mobiles.")]
|
|
public static void Stuck_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.Target = new StuckMenuTarget();
|
|
}
|
|
|
|
[Usage("Light <level>")]
|
|
[Description("Set your local lightlevel.")]
|
|
public static void Light_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.LightLevel = e.GetInt32(0);
|
|
}
|
|
|
|
[Usage("Stats")]
|
|
[Description("View some stats about the server.")]
|
|
public static void Stats_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.SendMessage($"Open Connections: {NetState.Instances.Count}");
|
|
e.Mobile.SendMessage($"Mobiles: {World.Mobiles.Count}");
|
|
e.Mobile.SendMessage($"Items: {World.Items.Count}");
|
|
}
|
|
|
|
private class ViewEqTarget : Target
|
|
{
|
|
public ViewEqTarget() : base(-1, false, TargetFlags.None)
|
|
{
|
|
}
|
|
|
|
protected override void OnTarget(Mobile from, object targeted)
|
|
{
|
|
if (!BaseCommand.IsAccessible(from, targeted))
|
|
{
|
|
from.SendLocalizedMessage(500447); // That is not accessible.
|
|
return;
|
|
}
|
|
|
|
if (targeted is Mobile mobile)
|
|
{
|
|
from.SendMenu(new EquipMenu(from, mobile, GetEquip(mobile)));
|
|
}
|
|
}
|
|
|
|
private static ItemListEntry[] GetEquip(Mobile m)
|
|
{
|
|
var entries = new ItemListEntry[m.Items.Count];
|
|
|
|
for (var i = 0; i < m.Items.Count; ++i)
|
|
{
|
|
var item = m.Items[i];
|
|
|
|
entries[i] = new ItemListEntry($"{item.Layer}: {item.GetType().Name}", item.ItemID, item.Hue);
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
private class EquipMenu : ItemListMenu
|
|
{
|
|
private readonly Mobile m_Mobile;
|
|
|
|
public EquipMenu(Mobile from, Mobile m, ItemListEntry[] entries) : base("Equipment", entries)
|
|
{
|
|
m_Mobile = m;
|
|
|
|
CommandLogging.WriteLine(
|
|
from,
|
|
$"{from.AccessLevel} {CommandLogging.Format(from)} viewing equipment of {CommandLogging.Format(m)}"
|
|
);
|
|
}
|
|
|
|
public override void OnResponse(NetState state, int index)
|
|
{
|
|
if (index >= 0 && index < m_Mobile.Items.Count)
|
|
{
|
|
var item = m_Mobile.Items[index];
|
|
|
|
state.Mobile.SendMenu(new EquipDetailsMenu(m_Mobile, item));
|
|
}
|
|
}
|
|
|
|
private class EquipDetailsMenu : QuestionMenu
|
|
{
|
|
private readonly Item m_Item;
|
|
private readonly Mobile m_Mobile;
|
|
|
|
public EquipDetailsMenu(Mobile m, Item item) : base(
|
|
$"{item.Layer}: {item.GetType().Name}",
|
|
new[] { "Move", "Delete", "Props" }
|
|
)
|
|
{
|
|
m_Mobile = m;
|
|
m_Item = item;
|
|
}
|
|
|
|
public override void OnCancel(NetState state)
|
|
{
|
|
state.Mobile.SendMenu(new EquipMenu(state.Mobile, m_Mobile, GetEquip(m_Mobile)));
|
|
}
|
|
|
|
public override void OnResponse(NetState state, int index)
|
|
{
|
|
if (index == 0)
|
|
{
|
|
CommandLogging.WriteLine(
|
|
state.Mobile,
|
|
$"{state.Mobile.AccessLevel} {CommandLogging.Format(state.Mobile)} moving equipment item {CommandLogging.Format(m_Item)} of {CommandLogging.Format(m_Mobile)}"
|
|
);
|
|
state.Mobile.Target = new MoveTarget(m_Item);
|
|
}
|
|
else if (index == 1)
|
|
{
|
|
CommandLogging.WriteLine(
|
|
state.Mobile,
|
|
$"{state.Mobile.AccessLevel} {CommandLogging.Format(state.Mobile)} deleting equipment item {CommandLogging.Format(m_Item)} of {CommandLogging.Format(m_Mobile)}"
|
|
);
|
|
m_Item.Delete();
|
|
}
|
|
else if (index == 2)
|
|
{
|
|
CommandLogging.WriteLine(
|
|
state.Mobile,
|
|
$"{state.Mobile.AccessLevel} {CommandLogging.Format(state.Mobile)} opening properties for equipment item {CommandLogging.Format(m_Item)} of {CommandLogging.Format(m_Mobile)}"
|
|
);
|
|
state.Mobile.SendGump(new PropertiesGump(state.Mobile, m_Item));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private class BankTarget : Target
|
|
{
|
|
public BankTarget() : base(-1, false, TargetFlags.None)
|
|
{
|
|
}
|
|
|
|
protected override void OnTarget(Mobile from, object targeted)
|
|
{
|
|
if (targeted is Mobile m)
|
|
{
|
|
var box = m.Player ? m.BankBox : m.FindBankNoCreate();
|
|
|
|
if (box != null)
|
|
{
|
|
CommandLogging.WriteLine(
|
|
from,
|
|
$"{from.AccessLevel} {CommandLogging.Format(from)} opening bank box of {CommandLogging.Format(m)}"
|
|
);
|
|
|
|
if (from == m)
|
|
{
|
|
box.Open();
|
|
}
|
|
else
|
|
{
|
|
box.DisplayTo(from);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("They have no bank box.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private class ClientTarget : Target
|
|
{
|
|
public ClientTarget() : base(-1, false, TargetFlags.None)
|
|
{
|
|
}
|
|
|
|
protected override void OnTarget(Mobile from, object targeted)
|
|
{
|
|
if (targeted is Mobile targ && targ.NetState != null)
|
|
{
|
|
CommandLogging.WriteLine(
|
|
from,
|
|
$"{from.AccessLevel} {CommandLogging.Format(from)} opening client menu of {CommandLogging.Format(targ)}"
|
|
);
|
|
from.SendGump(new ClientGump(from, targ.NetState));
|
|
}
|
|
}
|
|
}
|
|
|
|
private class StuckMenuTarget : Target
|
|
{
|
|
public StuckMenuTarget() : base(-1, false, TargetFlags.None)
|
|
{
|
|
}
|
|
|
|
protected override void OnTarget(Mobile from, object targeted)
|
|
{
|
|
if (targeted is Mobile mobile)
|
|
{
|
|
if (mobile.AccessLevel >= from.AccessLevel && mobile != from)
|
|
{
|
|
from.SendMessage("You can't do that to someone with higher Accesslevel than you!");
|
|
}
|
|
else
|
|
{
|
|
from.SendGump(new StuckMenu(from, mobile, false));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|