ModernUO/Projects/UOContent/Gumps/AdminGump.cs
Kamron Batman a7e65aab01
perf(login): run password hashing on a parked worker thread (#2566)
## Why

An Argon2 verify is **~8.9 ms of frozen world per login attempt** — more than half a 16 ms frame. Failed attempts cost exactly the same as successful ones, by design, so a credential-stuffing flood is a full-cost stall per packet without needing valid credentials. `SetPassword` derives a hash too, so `[password`, the admin gump and account creation each pay the same.

## What the measurement says

Off-loading does not delete the cost, it relocates it. Three things stay on the loop:

| Component | Measured |
|---|---:|
| Inline verify (today) | **8.92 ms** |
| Dispatch to the worker | 210 ns |
| Drain the continuation off `LoopContext` | 13 ns |
| Loop's own work slowed by shared-L3 eviction | **0.05 – 5.44 ms** |

Net gain **3.5 – 8.9 ms** of on-loop time per login. Harness in `ModernUO-Benchmarks` (`Benchmarks/Argon2OffLoop/`): it models the loop as a dependent-load pointer chase swept across working-set sizes, which is an upper bound on cache-latency sensitivity, and copies `EventLoopContext` so the hand-off cost is the real one.

Two results shaped the design:

- **The contention tax peaks in the middle of the working-set range**, not at the top — 5.44 ms at 8 MiB (a quarter of this chip's L3), but 0.76 ms at 30 MiB and 0.10 ms at 256 KiB. A tiny hot set has nothing in L3 to lose; a huge one is already DRAM-bound.
- **Per-login tax falls as concurrency rises** (5.44 → 2.56 → 1.60 ms at 1/2/4 hashers) while *total* loop damage rises. Contention is shared, not additive, so a login rush is not the disaster case — a single login is.

## Why exactly one worker

It is load-bearing three times over, which is also why it must not quietly become a pool:

- **Cost bound.** Off-loop loses to inline only if a hash steals ~82% of the loop's throughput. One hasher contending for one core leaves the loop ~50%. **A single background hasher cannot cost the loop more than the inline verify under any scheduling regime**, which is what lets the measurement hold on hardware we cannot inspect — AMD, VPS, oversubscribed VM. Four hashers drop the loop to ~20% and break it.
- **Memory.** Exactly one hashing arena is live at a time whatever the login volume.
- **Ordering.** Writes apply in dispatch order *only* because a single thread drains FIFO. A second worker would need ordering reintroduced; `WritesApplyInDispatchOrder` fails if that happens.

Throughput is ~110 verifies/sec. Only loop time matters, not login latency, so head-of-line blocking during a rush costs nothing.

## Making every protection safe off-thread

The worker was initially Argon2-only. That was the right call for the wrong reason — it was blamed on Argon2's salt RNG, which is a stateless syscall wrapper and was never a problem. The real blockers were elsewhere, and both are fixed at the source:

| Protection | Was | Now |
|---|---|---|
| MD5/SHA1/SHA2 | shared `HashAlgorithm.ComputeHash`, which carries the running digest across `HashCore`/`HashFinal` through process-wide singletons | static `HashData` into a `stackalloc` span — no state, no allocation, identical bytes |
| PBKDF2 | `Utility.RandomMinMax` → shared `System.Random`, thread-unsafe *and* game state | `RandomNumberGenerator.GetInt32`, matching the salt beside it |
| Argon2 | already safe (`Verify` is static + stackalloc) | unchanged, singleton reused |

Literal digests are pinned in a test **before** the change and still pass after it. These are compared as strings against every account database, so any casing or encoding drift would lock out every SHA and MD5 account at once.

With all three safe, the worker no longer knows which algorithm it runs and the dispatch conditions collapse to "is off-loop available".

## Correctness

- **Phrase derivation** moves to `AccountSecurity.DerivePhrase`, so verification (stored algorithm's rule) and rehash (target algorithm's rule) cannot disagree. Deriving with the wrong one is the shape of the lockout fixed in #2562.
- **Liveness** is checked at dequeue *and* at apply — a connection can drop while queued or while the result sits in the loop queue. A job with no connection attached, such as an admin password change, runs regardless.
- **Queue overflow rejects** a login rather than verifying inline; steering work back onto the loop is what a flood wants. A password change instead falls back to hashing inline, because unlike a login it must not be dropped.
- **Shutdown and crash** both just stop the thread, and pending jobs are dropped. No save is initiated once shutdown begins — saving is the operator's choice up front, via the admin gump's save/no-save variants, and `WaitForWriteCompletion` honours one already in flight — so a write applied during teardown would reach no disk. The crash path needs its own subscription because `HandleClosed` skips `InvokeShutdown` when crashed.

## Bounding

`MaxPending` is 4096 — a backstop, not a flood defense. `SentFirstPacket` holds a connection to one pending verify and the engine caps connections at 4096, so the queue is already bounded by construction and this can only trip if that invariant breaks. A cap low enough to blunt an attack would reject real players first; during a mass reconnect they *are* the queue. Flood defense belongs at the connection layer.

The real DoS improvement is elsewhere: today every attempt stalls the world, and after this a flood occupies one core while the loop keeps ticking.

## Gate

Release builds on 4+ cores. Below that there is no spare core to move work to, so off-loading buys nothing by construction; `DEBUG` is excluded because dev boxes and test shards have few logins. Both modes call the same code — the gate only chooses where it runs.

## Engine change

One property, `AccountLoginEventArgs.Deferred`, so a subscriber can say "no verdict yet". `EventSink.AccountLogin` is `Action<...>` with no continuation, and the packet handler replies in the same call. Approved separately since it touches `Projects/Server/`.

## Docs

`dev-docs/threading-model.md` and the threading skill gain a vetted-workers section. The forbidden-patterns table bans `new Thread`, `ConcurrentQueue<T>`, `Interlocked` and `volatile` in `UOContent`, and its exceptions covered only `Projects/Server/` — the existing Advanced Search fan-out already sat outside it. The new section leads with proving the need (measure on-loop time, not wall-clock; gate on core count; record the measurement), keeps game logic on the loop via chunking, and documents the hand-off protocol in both directions.

## Testing

698 UOContent tests, 810 Server tests, Release build clean.

Covered: verify and rehash outcomes, phrase rules for SHA1/SHA2 vs Argon2, stored-format stability for MD5/SHA1/SHA2, jobs with no connection attached, and dispatch ordering through the real queue. The liveness and ordering guards are mutation-verified.
2026-08-09 00:13:34 -07:00

4358 lines
182 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Threading;
using Server.Accounting;
using Server.Accounting.Security;
using Server.Collections;
using Server.Commands;
using Server.Maps;
using Server.Misc;
using Server.Multis;
using Server.Network;
using Server.Network.Bans;
using Server.Prompts;
using Server.Saves;
using Server.Text;
namespace Server.Gumps
{
public enum AdminGumpPage
{
Information_General,
Information_Perf,
Administer,
Clients,
Accounts,
Accounts_Shared,
Firewall,
Administer_WorldBuilding,
Administer_Server,
Administer_Access,
Administer_Access_Lockdown,
Administer_Commands,
ClientInfo,
AccountDetails,
AccountDetails_Information,
AccountDetails_Characters,
AccountDetails_Access,
AccountDetails_Access_ClientIPs,
AccountDetails_Comments,
AccountDetails_Tags,
AccountDetails_ChangePassword,
AccountDetails_ChangeAccess,
FirewallInfo
}
public class AdminGump : Gump
{
private const int LabelColor = 0x7FFF;
private const int SelectedColor = 0x421F;
private const int DisabledColor = 0x4210;
private const int LabelColor32 = 0xFFFFFF;
private const int SelectedColor32 = 0x8080FF;
private const int DisabledColor32 = 0x808080;
private const int LabelHue = 0x480;
private const int GreenHue = 0x40;
private const int RedHue = 0x20;
private static readonly string[] m_AccessLevelStrings =
[
"Player",
"Counselor",
"Game Master",
"Seer",
"Administrator",
"Developer",
"Owner"
];
private readonly Mobile m_From;
private readonly List<object> m_List;
private readonly int m_ListPage;
private readonly AdminGumpPage m_PageType;
private readonly object m_State;
public override bool Singleton => true;
public AdminGump(
Mobile from, AdminGumpPage pageType, int listPage = 0, List<object> list = null, string notice = null,
object state = null
) : base(50, 40)
{
m_From = from;
m_PageType = pageType;
m_ListPage = listPage;
m_State = state;
m_List = list;
FilterAccess(m_List, from);
AddPage(0);
AddBackground(0, 0, 420, 480, 5054);
AddBlackAlpha(10, 10, 170, 100);
AddBlackAlpha(190, 10, 220, 100);
AddBlackAlpha(10, 120, 400, 300);
AddBlackAlpha(10, 430, 400, 40);
AddPageButton(
10,
10,
GetButtonID(0, 0),
"INFORMATION",
AdminGumpPage.Information_General,
AdminGumpPage.Information_Perf
);
AddPageButton(
10,
30,
GetButtonID(0, 1),
"ADMINISTER",
AdminGumpPage.Administer,
AdminGumpPage.Administer_Access,
AdminGumpPage.Administer_Commands,
AdminGumpPage.Administer_Server,
AdminGumpPage.Administer_WorldBuilding,
AdminGumpPage.Administer_Access_Lockdown
);
AddPageButton(10, 50, GetButtonID(0, 2), "CLIENT LIST", AdminGumpPage.Clients, AdminGumpPage.ClientInfo);
AddPageButton(
10,
70,
GetButtonID(0, 3),
"ACCOUNT LIST",
AdminGumpPage.Accounts,
AdminGumpPage.Accounts_Shared,
AdminGumpPage.AccountDetails,
AdminGumpPage.AccountDetails_Information,
AdminGumpPage.AccountDetails_Characters,
AdminGumpPage.AccountDetails_Access,
AdminGumpPage.AccountDetails_Access_ClientIPs,
AdminGumpPage.AccountDetails_Comments,
AdminGumpPage.AccountDetails_Tags,
AdminGumpPage.AccountDetails_ChangeAccess,
AdminGumpPage.AccountDetails_ChangePassword
);
AddPageButton(10, 90, GetButtonID(0, 4), "FIREWALL", AdminGumpPage.Firewall, AdminGumpPage.FirewallInfo);
if (notice != null)
{
AddHtml(20, 440, 396, 36, notice.Color(LabelColor32));
}
switch (pageType)
{
case AdminGumpPage.Information_General:
{
var banned = 0;
var active = 0;
foreach (Account acct in Accounts.GetAccounts())
{
if (acct.Banned)
{
++banned;
}
else
{
++active;
}
}
AddLabel(20, 130, LabelHue, "Active Accounts:");
AddLabel(150, 130, LabelHue, active.ToString());
AddLabel(20, 150, LabelHue, "Banned Accounts:");
AddLabel(150, 150, LabelHue, banned.ToString());
AddLabel(20, 170, LabelHue, "Firewalled:");
AddLabel(150, 170, LabelHue, Firewall.FirewallSetCount.ToString());
AddLabel(20, 190, LabelHue, "Clients:");
AddLabel(150, 190, LabelHue, NetState.Instances.Count.ToString());
AddLabel(20, 210, LabelHue, "Mobiles:");
AddLabel(150, 210, LabelHue, World.Mobiles.Count.ToString());
AddLabel(20, 230, LabelHue, "Mobile Scripts:");
AddLabel(150, 230, LabelHue, Core.ScriptMobiles.ToString());
AddLabel(20, 250, LabelHue, "Items:");
AddLabel(150, 250, LabelHue, World.Items.Count.ToString());
AddLabel(20, 270, LabelHue, "Item Scripts:");
AddLabel(150, 270, LabelHue, Core.ScriptItems.ToString());
AddLabel(20, 290, LabelHue, "Uptime:");
AddLabel(150, 290, LabelHue, FormatTimeSpan(TimeSpan.FromMilliseconds(Core.Uptime)));
AddLabel(20, 310, LabelHue, "Memory:");
AddLabel(150, 310, LabelHue, FormatByteAmount(GC.GetTotalMemory(false)));
AddLabel(20, 330, LabelHue, "Framework:");
AddLabel(150, 330, LabelHue, Environment.Version.ToString());
AddLabel(20, 350, LabelHue, "Operating System: ");
var os = Environment.OSVersion.ToString();
os = os.ReplaceOrdinal("Microsoft", "MSFT");
os = os.ReplaceOrdinal("Service Pack", "SP");
AddLabel(150, 350, LabelHue, os);
/*string str;
try{ str = FormatTimeSpan( Core.Process.TotalProcessorTime ); }
catch{ str = "(unable to retrieve)"; }
AddLabel( 20, 330, LabelHue, "Process Time:" );
AddLabel( 250, 330, LabelHue, str );*/
/*try{ str = Core.Process.PriorityClass.ToString(); }
catch{ str = "(unable to retrieve)"; }
AddLabel( 20, 350, LabelHue, "Process Priority:" );
AddLabel( 250, 350, LabelHue, str );*/
AddPageButton(200, 20, GetButtonID(0, 0), "General", AdminGumpPage.Information_General);
AddPageButton(200, 40, GetButtonID(0, 5), "Performance", AdminGumpPage.Information_Perf);
break;
}
case AdminGumpPage.Information_Perf:
{
AddLabel(20, 130, LabelHue, "Cycles Per Second:");
AddLabel(40, 150, LabelHue, $"Current: {Core.CyclesPerSecond:N2}");
AddLabel(40, 170, LabelHue, $"Average: {Core.AverageCPS:N2}");
using var sb = ValueStringBuilder.Create();
ThreadPool.GetAvailableThreads(out var curUser, out var curIOCP);
ThreadPool.GetMaxThreads(out var maxUser, out var maxIOCP);
sb.Append("Worker Threads:<br>Capacity: ");
sb.Append(maxUser);
sb.Append("<br>Available: ");
sb.Append(curUser);
sb.Append("<br>Usage: ");
sb.Append((maxUser - curUser) * 100 / maxUser);
sb.Append("%<br><br>IOCP Threads:<br>Capacity: ");
sb.Append(maxIOCP);
sb.Append("<br>Available: ");
sb.Append(curIOCP);
sb.Append("<br>Usage: ");
sb.Append((maxIOCP - curIOCP) * 100 / maxIOCP);
sb.Append('%');
AddLabel(20, 200, LabelHue, "Pooling:");
AddHtml(20, 220, 380, 150, sb.ToString(), true, true);
AddPageButton(200, 20, GetButtonID(0, 0), "General", AdminGumpPage.Information_General);
AddPageButton(200, 40, GetButtonID(0, 5), "Performance", AdminGumpPage.Information_Perf);
break;
}
case AdminGumpPage.Administer_WorldBuilding:
{
AddHtml(10, 125, 400, 20, "Generating".Center(LabelColor32));
AddButtonLabeled(20, 175, GetButtonID(3, 101), "Teleporters");
AddButtonLabeled(220, 175, GetButtonID(3, 102), "Moongates");
AddButtonLabeled(20, 200, GetButtonID(3, 103), "Spawners");
AddButtonLabeled(220, 200, GetButtonID(3, 106), "Decorations");
AddButtonLabeled(20, 225, GetButtonID(3, 104), "Doors");
AddButtonLabeled(220, 225, GetButtonID(3, 105), "Signs");
AddButtonLabeled(20, 250, GetButtonID(3, 107), "Champions");
AddButtonLabeled(220, 250, GetButtonID(3, 108), "Magincia (AOS+)");
AddButtonLabeled(20, 275, GetButtonID(3, 109), "Stealable Artifacts");
AddButtonLabeled(220, 275, GetButtonID(3, 110), "SH Teleporters");
AddButtonLabeled(20, 300, GetButtonID(3, 111), "Secret Locations");
AddButtonLabeled(220, 300, GetButtonID(3, 112), "Doom");
AddButtonLabeled(20, 325, GetButtonID(3, 113), "Khaldun Puzzles");
AddButtonLabeled(220, 325, GetButtonID(3, 114), "Do everything");
goto case AdminGumpPage.Administer;
}
case AdminGumpPage.Administer_Server:
{
AddHtml(10, 125, 400, 20, "Server".Center(LabelColor32));
AddButtonLabeled(20, 150, GetButtonID(3, 200), "Save");
/*if (!Core.Service)
{*/
AddButtonLabeled(20, 180, GetButtonID(3, 201), "Shutdown (With Save)");
AddButtonLabeled(20, 200, GetButtonID(3, 202), "Shutdown (Without Save)");
AddButtonLabeled(20, 230, GetButtonID(3, 203), "Shutdown & Restart (With Save)");
AddButtonLabeled(20, 250, GetButtonID(3, 204), "Shutdown & Restart (Without Save)");
AddButtonLabeled(20, 270, GetButtonID(3, 205), "Shutdown (With 15m Delay & Save)");
/*}
else
{
AddLabel( 20, 215, LabelHue, "Shutdown/Restart not available." );
}*/
AddHtml(10, 295, 400, 20, "Broadcast".Center(LabelColor32));
AddTextField(20, 320, 380, 20, 0);
AddButtonLabeled(20, 350, GetButtonID(3, 210), "To Everyone");
AddButtonLabeled(220, 350, GetButtonID(3, 211), "To Staff");
goto case AdminGumpPage.Administer;
}
case AdminGumpPage.Administer_Access_Lockdown:
{
AddHtml(10, 125, 400, 20, "Server Lockdown".Center(LabelColor32));
AddHtml(
20,
150,
380,
80,
"When enabled, only clients with an access level equal to or greater than the specified lockdown level may access the server. After setting a lockdown level, use the <em>Purge Invalid Clients</em> button to disconnect those clients without access.".Color(
LabelColor32
)
);
var level = AccountHandler.LockdownLevel;
var isLockedDown = level > AccessLevel.Player;
AddSelectedButton(20, 230, GetButtonID(3, 500), "Not Locked Down", !isLockedDown);
AddSelectedButton(
20,
260,
GetButtonID(3, 504),
"Administrators",
isLockedDown && level <= AccessLevel.Administrator
);
AddSelectedButton(20, 280, GetButtonID(3, 503), "Seers", isLockedDown && level <= AccessLevel.Seer);
AddSelectedButton(
20,
300,
GetButtonID(3, 502),
"Game Masters",
isLockedDown && level <= AccessLevel.GameMaster
);
AddSelectedButton(
20,
320,
GetButtonID(3, 501),
"Counselors",
isLockedDown && level <= AccessLevel.Counselor
);
AddButtonLabeled(20, 350, GetButtonID(3, 510), "Purge Invalid Clients");
goto case AdminGumpPage.Administer;
}
case AdminGumpPage.Administer_Access:
{
AddHtml(10, 125, 400, 20, "Access".Center(LabelColor32));
AddHtml(10, 155, 400, 20, "Connectivity".Center(LabelColor32));
AddButtonLabeled(20, 180, GetButtonID(3, 300), "Kick");
AddButtonLabeled(220, 180, GetButtonID(3, 301), "Ban");
AddButtonLabeled(20, 210, GetButtonID(3, 302), "Firewall");
AddButtonLabeled(220, 210, GetButtonID(3, 303), "Lockdown");
AddHtml(10, 245, 400, 20, "Staff".Center(LabelColor32));
AddButtonLabeled(20, 270, GetButtonID(3, 310), "Make Player");
AddButtonLabeled(20, 290, GetButtonID(3, 311), "Make Counselor");
AddButtonLabeled(20, 310, GetButtonID(3, 312), "Make Game Master");
AddButtonLabeled(20, 330, GetButtonID(3, 313), "Make Seer");
if (from.AccessLevel > AccessLevel.Administrator)
{
AddButtonLabeled(220, 270, GetButtonID(3, 314), "Make Administrator");
if (from.AccessLevel > AccessLevel.Developer)
{
AddButtonLabeled(220, 290, GetButtonID(3, 315), "Make Developer");
if (from.AccessLevel >= AccessLevel.Owner)
{
AddButtonLabeled(220, 310, GetButtonID(3, 316), "Make Owner");
}
}
}
goto case AdminGumpPage.Administer;
}
case AdminGumpPage.Administer_Commands:
{
AddHtml(10, 125, 400, 20, "Commands".Center(LabelColor32));
AddButtonLabeled(20, 150, GetButtonID(3, 400), "Add");
AddButtonLabeled(220, 150, GetButtonID(3, 401), "Remove");
AddButtonLabeled(20, 170, GetButtonID(3, 402), "Dupe");
AddButtonLabeled(220, 170, GetButtonID(3, 403), "Dupe in bag");
AddButtonLabeled(20, 200, GetButtonID(3, 404), "Properties");
AddButtonLabeled(220, 200, GetButtonID(3, 405), "Skills");
AddButtonLabeled(20, 230, GetButtonID(3, 406), "Mortal");
AddButtonLabeled(220, 230, GetButtonID(3, 407), "Immortal");
AddButtonLabeled(20, 250, GetButtonID(3, 408), "Squelch");
AddButtonLabeled(220, 250, GetButtonID(3, 409), "Unsquelch");
AddButtonLabeled(20, 290, GetButtonID(3, 412), "Hide");
AddButtonLabeled(220, 290, GetButtonID(3, 413), "Unhide");
AddButtonLabeled(20, 310, GetButtonID(3, 414), "Kill");
AddButtonLabeled(220, 310, GetButtonID(3, 415), "Resurrect");
AddButtonLabeled(20, 330, GetButtonID(3, 416), "Move");
AddButtonLabeled(220, 330, GetButtonID(3, 417), "Wipe");
AddButtonLabeled(20, 350, GetButtonID(3, 418), "Teleport");
AddButtonLabeled(220, 350, GetButtonID(3, 419), "Teleport (Multiple)");
goto case AdminGumpPage.Administer;
}
case AdminGumpPage.Administer:
{
AddPageButton(200, 20, GetButtonID(3, 0), "World Building", AdminGumpPage.Administer_WorldBuilding);
AddPageButton(200, 40, GetButtonID(3, 1), "Server", AdminGumpPage.Administer_Server);
AddPageButton(
200,
60,
GetButtonID(3, 2),
"Access",
AdminGumpPage.Administer_Access,
AdminGumpPage.Administer_Access_Lockdown
);
AddPageButton(200, 80, GetButtonID(3, 3), "Commands", AdminGumpPage.Administer_Commands);
break;
}
case AdminGumpPage.Clients:
{
if (m_List == null)
{
var states = NetState.Instances.ToList();
states.Sort(NetStateComparer.Instance);
m_List = states.ToList<object>();
FilterAccess(m_List, from);
}
AddClientHeader();
AddLabelCropped(12, 120, 81, 20, LabelHue, "Name");
AddLabelCropped(95, 120, 81, 20, LabelHue, "Account");
AddLabelCropped(178, 120, 81, 20, LabelHue, "Access Level");
AddLabelCropped(273, 120, 109, 20, LabelHue, "IP Address");
if (listPage > 0)
{
AddButton(375, 122, 0x15E3, 0x15E7, GetButtonID(1, 0));
}
else
{
AddImage(375, 122, 0x25EA);
}
if ((listPage + 1) * 12 < m_List.Count)
{
AddButton(392, 122, 0x15E1, 0x15E5, GetButtonID(1, 1));
}
else
{
AddImage(392, 122, 0x25E6);
}
if (m_List.Count == 0)
{
AddLabel(12, 140, LabelHue, "There are no clients to display.");
}
for (int i = 0, index = listPage * 12; i < 12 && index >= 0 && index < m_List.Count; ++i, ++index)
{
if (m_List[index] is not NetState ns)
{
continue;
}
var m = ns.Mobile;
var a = ns.Account as Account;
var offset = 140 + i * 20;
if (m == null)
{
AddLabelCropped(12, offset, 81, 20, LabelHue, "(logging in)");
}
else
{
AddLabelCropped(12, offset, 81, 20, GetHueFor(m), m.Name);
}
AddLabelCropped(95, offset, 81, 20, LabelHue, a == null ? "(no account)" : a.Username);
AddLabelCropped(
178,
offset,
81,
20,
LabelHue,
m == null
? a != null ? FormatAccessLevel(a.AccessLevel) : ""
: FormatAccessLevel(m.AccessLevel)
);
AddLabelCropped(273, offset, 109, 20, LabelHue, ns.ToString());
if (a != null || m != null)
{
AddButton(380, offset - 1, 0xFA5, 0xFA7, GetButtonID(4, index + 2));
}
}
break;
}
case AdminGumpPage.ClientInfo:
{
if (state is not Mobile m)
{
break;
}
AddClientHeader();
AddHtml(10, 125, 400, 20, "Information".Center(LabelColor32));
var y = 146;
AddLabel(20, y, LabelHue, "Name:");
AddLabel(200, y, GetHueFor(m), m.Name);
y += 20;
var a = m.Account as Account;
AddLabel(20, y, LabelHue, "Account:");
AddLabel(200, y, a?.Banned == true ? RedHue : LabelHue, a == null ? "(no account)" : a.Username);
if (m.AccessLevel > from.AccessLevel || a?.AccessLevel > from.AccessLevel)
{
AddLabel(20, y + 20, LabelHue, "You do not have permission to view this client's information.");
break;
}
AddButton(380, y, 0xFA5, 0xFA7, GetButtonID(7, 14));
y += 20;
var ns = m.NetState;
if (ns == null)
{
AddLabel(20, y, LabelHue, "Address:");
AddLabel(200, y, RedHue, "Offline");
y += 20;
AddLabel(20, y, LabelHue, "Location:");
AddLabel(200, y, LabelHue, $"{m.Location} [{m.Map}]");
y += 44;
}
else
{
AddLabel(20, y, LabelHue, "Address:");
AddLabel(200, y, GreenHue, ns.ToString());
y += 20;
var v = ns.Version;
AddLabel(20, y, LabelHue, "Version:");
if (ns.Assistant != null)
{
AddLabel(200, y, LabelHue, $"{v.SourceString ?? "(null)"} ({ns.Assistant})");
}
else
{
AddLabel(200, y, LabelHue, v.SourceString ?? "(null)");
}
y += 20;
AddLabel(20, y, LabelHue, "Location:");
AddLabel(200, y, LabelHue, $"{m.Location} [{m.Map}]");
y += 24;
}
AddButtonLabeled(20, y, GetButtonID(7, 0), "Go to");
AddButtonLabeled(200, y, GetButtonID(7, 1), "Get");
y += 20;
AddButtonLabeled(20, y, GetButtonID(7, 2), "Kick");
AddButtonLabeled(200, y, GetButtonID(7, 3), "Ban");
y += 20;
AddButtonLabeled(20, y, GetButtonID(7, 4), "Properties");
AddButtonLabeled(200, y, GetButtonID(7, 5), "Skills");
y += 20;
AddButtonLabeled(20, y, GetButtonID(7, 6), "Mortal");
AddButtonLabeled(200, y, GetButtonID(7, 7), "Immortal");
y += 20;
AddButtonLabeled(20, y, GetButtonID(7, 8), "Squelch");
AddButtonLabeled(200, y, GetButtonID(7, 9), "Unsquelch");
y += 20;
/*AddButtonLabeled( 20, y, GetButtonID( 7, 10 ), "Hide" );
AddButtonLabeled( 200, y, GetButtonID( 7, 11 ), "Unhide" );
y += 20;*/
AddButtonLabeled(20, y, GetButtonID(7, 12), "Kill");
AddButtonLabeled(200, y, GetButtonID(7, 13), "Resurrect");
y += 20;
AddButtonLabeled(20, y, GetButtonID(7, 15), "Jail");
AddButtonLabeled(200, y, GetButtonID(7, 16), "Unjail");
y += 25;
AddLabel(20, y, LabelHue, "Jail Reason:");
AddTextField(100, y, 300, 20, 1);
break;
}
case AdminGumpPage.Accounts_Shared:
{
m_List ??= GetAllSharedAccounts();
AddLabelCropped(12, 120, 60, 20, LabelHue, "Count");
AddLabelCropped(72, 120, 120, 20, LabelHue, "Address");
AddLabelCropped(192, 120, 180, 20, LabelHue, "Accounts");
if (listPage > 0)
{
AddButton(375, 122, 0x15E3, 0x15E7, GetButtonID(1, 0));
}
else
{
AddImage(375, 122, 0x25EA);
}
if ((listPage + 1) * 12 < m_List.Count)
{
AddButton(392, 122, 0x15E1, 0x15E5, GetButtonID(1, 1));
}
else
{
AddImage(392, 122, 0x25E6);
}
if (m_List.Count == 0)
{
AddLabel(12, 140, LabelHue, "There are no accounts to display.");
}
using var sb = ValueStringBuilder.Create();
for (int i = 0, index = listPage * 12; i < 12 && index >= 0 && index < m_List.Count; ++i, ++index)
{
var (ipAddr, accts) = (KeyValuePair<IPAddress, List<Account>>)m_List[index];
var offset = 140 + i * 20;
AddLabelCropped(12, offset, 60, 20, LabelHue, accts.Count.ToString());
AddLabelCropped(72, offset, 120, 20, LabelHue, ipAddr.ToString());
sb.Reset();
for (var j = 0; j < accts.Count; ++j)
{
if (j > 0)
{
sb.Append(", ");
}
if (j < 4)
{
var acct = accts[j];
sb.Append(acct.Username);
}
else
{
sb.Append("...");
break;
}
}
AddLabelCropped(192, offset, 180, 20, LabelHue, sb.ToString());
AddButton(380, offset - 1, 0xFA5, 0xFA7, GetButtonID(5, index + 56));
}
break;
}
case AdminGumpPage.Accounts:
{
m_List ??= [];
var rads = state as List<Account>;
AddAccountHeader();
if (rads == null)
{
AddLabelCropped(12, 120, 120, 20, LabelHue, "Name");
}
else
{
AddLabelCropped(32, 120, 100, 20, LabelHue, "Name");
}
AddLabelCropped(132, 120, 120, 20, LabelHue, "Access Level");
AddLabelCropped(252, 120, 120, 20, LabelHue, "Status");
if (listPage > 0)
{
AddButton(375, 122, 0x15E3, 0x15E7, GetButtonID(1, 0));
}
else
{
AddImage(375, 122, 0x25EA);
}
if ((listPage + 1) * 12 < m_List.Count)
{
AddButton(392, 122, 0x15E1, 0x15E5, GetButtonID(1, 1));
}
else
{
AddImage(392, 122, 0x25E6);
}
if (m_List.Count == 0)
{
AddLabel(12, 140, LabelHue, "There are no accounts to display.");
}
if (rads != null && notice == null)
{
AddButtonLabeled(10, 390, GetButtonID(5, 27), "Ban marked");
AddButtonLabeled(10, 410, GetButtonID(5, 28), "Delete marked");
AddButtonLabeled(210, 390, GetButtonID(5, 29), "Mark all");
AddButtonLabeled(210, 410, GetButtonID(5, 35), "Unmark house owners");
}
for (int i = 0, index = listPage * 12; i < 12 && index >= 0 && index < m_List.Count; ++i, ++index)
{
if (m_List[index] is not Account a)
{
continue;
}
var offset = 140 + i * 20;
GetAccountInfo(a, out var accessLevel, out var online);
if (rads == null)
{
AddLabelCropped(12, offset, 120, 20, LabelHue, a.Username);
}
else
{
AddCheck(10, offset, 0xD2, 0xD3, rads.Contains(a), index);
AddLabelCropped(32, offset, 100, 20, LabelHue, a.Username);
}
AddLabelCropped(132, offset, 120, 20, LabelHue, FormatAccessLevel(accessLevel));
if (online)
{
AddLabelCropped(252, offset, 120, 20, GreenHue, "Online");
}
else if (a.Banned)
{
AddLabelCropped(252, offset, 120, 20, RedHue, "Banned");
}
else
{
AddLabelCropped(252, offset, 120, 20, RedHue, "Offline");
}
AddButton(380, offset - 1, 0xFA5, 0xFA7, GetButtonID(5, index + 56));
}
break;
}
case AdminGumpPage.AccountDetails:
{
AddPageButton(
190,
10,
GetButtonID(5, 0),
"Information",
AdminGumpPage.AccountDetails_Information,
AdminGumpPage.AccountDetails_ChangeAccess,
AdminGumpPage.AccountDetails_ChangePassword
);
AddPageButton(190, 30, GetButtonID(5, 1), "Characters", AdminGumpPage.AccountDetails_Characters);
AddPageButton(
190,
50,
GetButtonID(5, 13),
"Access",
AdminGumpPage.AccountDetails_Access,
AdminGumpPage.AccountDetails_Access_ClientIPs
);
AddPageButton(190, 70, GetButtonID(5, 2), "Comments", AdminGumpPage.AccountDetails_Comments);
AddPageButton(190, 90, GetButtonID(5, 3), "Tags", AdminGumpPage.AccountDetails_Tags);
break;
}
case AdminGumpPage.AccountDetails_ChangePassword:
{
if (state is not Account a)
{
break;
}
AddHtml(10, 125, 400, 20, "Change Password".Center(LabelColor32));
AddLabel(20, 150, LabelHue, "Username:");
AddLabel(200, 150, LabelHue, a.Username);
AddLabel(20, 180, LabelHue, "Password:");
AddTextField(200, 180, 160, 20, 0);
AddLabel(20, 210, LabelHue, "Confirm:");
AddTextField(200, 210, 160, 20, 1);
AddButtonLabeled(20, 240, GetButtonID(5, 12), "Submit Change");
goto case AdminGumpPage.AccountDetails;
}
case AdminGumpPage.AccountDetails_ChangeAccess:
{
if (state is not Account a)
{
break;
}
AddHtml(10, 125, 400, 20, "Change Access Level".Center(LabelColor32));
AddLabel(20, 150, LabelHue, "Username:");
AddLabel(200, 150, LabelHue, a.Username);
AddLabel(20, 170, LabelHue, "Current Level:");
AddLabel(200, 170, LabelHue, FormatAccessLevel(a.AccessLevel));
AddButtonLabeled(20, 200, GetButtonID(5, 20), "Player");
AddButtonLabeled(20, 220, GetButtonID(5, 21), "Counselor");
AddButtonLabeled(20, 240, GetButtonID(5, 22), "Game Master");
AddButtonLabeled(20, 260, GetButtonID(5, 23), "Seer");
if (from.AccessLevel > AccessLevel.Administrator)
{
AddButtonLabeled(20, 280, GetButtonID(5, 24), "Administrator");
if (from.AccessLevel > AccessLevel.Developer)
{
AddButtonLabeled(20, 300, GetButtonID(5, 33), "Developer");
if (from.AccessLevel >= AccessLevel.Owner)
{
AddButtonLabeled(20, 320, GetButtonID(5, 34), "Owner");
}
}
}
goto case AdminGumpPage.AccountDetails;
}
case AdminGumpPage.AccountDetails_Information:
{
if (state is not Account a)
{
break;
}
var charCount = 0;
for (var i = 0; i < a.Length; ++i)
{
if (a[i] != null)
{
++charCount;
}
}
AddHtml(10, 125, 400, 20, "Information".Center(LabelColor32));
AddLabel(20, 150, LabelHue, "Username:");
AddLabel(200, 150, LabelHue, a.Username);
AddLabel(20, 170, LabelHue, "Access Level:");
AddLabel(200, 170, LabelHue, FormatAccessLevel(a.AccessLevel));
AddLabel(20, 190, LabelHue, "Status:");
AddLabel(200, 190, a.Banned ? RedHue : GreenHue, a.Banned ? "Banned" : "Active");
if (a.Banned && a.GetBanTags(out var banTime, out var banDuration))
{
if (banDuration == TimeSpan.MaxValue)
{
AddLabel(250, 190, LabelHue, "(Infinite)");
}
else if (banDuration == TimeSpan.Zero)
{
AddLabel(250, 190, LabelHue, "(Zero)");
}
else
{
var remaining = (Core.Now - banTime).Clamp(TimeSpan.Zero, banDuration);
var remMinutes = remaining.TotalMinutes;
var totMinutes = banDuration.TotalMinutes;
var perc = remMinutes / totMinutes;
AddLabel(250, 190, LabelHue, $"{FormatTimeSpan(banDuration)} [{perc * 100:F0}%]");
}
}
else if (a.Banned)
{
AddLabel(250, 190, LabelHue, "(Unspecified)");
}
AddLabel(20, 210, LabelHue, "Created:");
AddLabel(200, 210, LabelHue, a.Created.ToString());
AddLabel(20, 230, LabelHue, "Last Login:");
AddLabel(200, 230, LabelHue, a.LastLogin.ToString());
AddLabel(20, 250, LabelHue, "Character Count:");
AddLabel(200, 250, LabelHue, charCount.ToString());
AddLabel(20, 270, LabelHue, "Comment Count:");
AddLabel(200, 270, LabelHue, a.Comments.Count.ToString());
AddLabel(20, 290, LabelHue, "Tag Count:");
AddLabel(200, 290, LabelHue, a.Tags.Count.ToString());
AddButtonLabeled(20, 320, GetButtonID(5, 8), "Change Password");
AddButtonLabeled(200, 320, GetButtonID(5, 9), "Change Access Level");
if (!a.Banned)
{
AddButtonLabeled(20, 350, GetButtonID(5, 10), "Ban Account");
}
else
{
AddButtonLabeled(20, 350, GetButtonID(5, 11), "Unban Account");
}
AddButtonLabeled(200, 350, GetButtonID(5, 25), "Delete Account");
goto case AdminGumpPage.AccountDetails;
}
case AdminGumpPage.AccountDetails_Access:
{
if (state is not Account a)
{
break;
}
AddHtml(10, 125, 400, 20, "Access".Center(LabelColor32));
AddPageButton(
20,
150,
GetButtonID(5, 14),
"View client addresses",
AdminGumpPage.AccountDetails_Access_ClientIPs
);
goto case AdminGumpPage.AccountDetails;
}
case AdminGumpPage.AccountDetails_Access_ClientIPs:
{
if (state is not Account a)
{
break;
}
m_List ??= a.LoginIPs.ToList<object>();
AddHtml(10, 195, 400, 20, "Client Addresses".Center(LabelColor32));
AddButtonLabeled(227, 225, GetButtonID(5, 16), "View all shared accounts");
AddButtonLabeled(227, 245, GetButtonID(5, 17), "Ban all shared accounts");
AddButtonLabeled(227, 265, GetButtonID(5, 18), "Firewall all addresses");
AddButtonLabeled(227, 285, GetButtonID(5, 36), "Clear all addresses");
AddHtml(
225,
315,
180,
80,
"List of IP addresses which have accessed this account.".Color(LabelColor32)
);
AddImageTiled(15, 219, 206, 156, 0xBBC);
AddBlackAlpha(16, 220, 204, 154);
AddHtml(18, 221, 114, 20, "IP Address".Color(LabelColor32));
if (listPage > 0)
{
AddButton(184, 223, 0x15E3, 0x15E7, GetButtonID(1, 0));
}
else
{
AddImage(184, 223, 0x25EA);
}
if ((listPage + 1) * 6 < m_List.Count)
{
AddButton(201, 223, 0x15E1, 0x15E5, GetButtonID(1, 1));
}
else
{
AddImage(201, 223, 0x25E6);
}
if (m_List.Count == 0)
{
AddHtml(18, 243, 200, 60, "This account has not yet been accessed.".Color(LabelColor32));
}
for (int i = 0, index = listPage * 6; i < 6 && index >= 0 && index < m_List.Count; ++i, ++index)
{
AddHtml(18, 243 + i * 22, 114, 20, Html.Color($"{m_List[index]}", LabelColor32));
AddButton(130, 242 + i * 22, 0xFA2, 0xFA4, GetButtonID(8, index));
AddButton(160, 242 + i * 22, 0xFA8, 0xFAA, GetButtonID(9, index));
AddButton(190, 242 + i * 22, 0xFB1, 0xFB3, GetButtonID(10, index));
}
goto case AdminGumpPage.AccountDetails_Access;
}
case AdminGumpPage.AccountDetails_Characters:
{
if (state is not Account a)
{
break;
}
AddHtml(10, 125, 400, 20, "Characters".Center(LabelColor32));
AddLabelCropped(12, 150, 120, 20, LabelHue, "Name");
AddLabelCropped(132, 150, 120, 20, LabelHue, "Access Level");
AddLabelCropped(252, 150, 120, 20, LabelHue, "Status");
var index = 0;
for (var i = 0; i < a.Length; ++i)
{
var m = a[i];
if (m == null)
{
continue;
}
var offset = 170 + index * 20;
AddLabelCropped(12, offset, 120, 20, GetHueFor(m), m.Name);
AddLabelCropped(132, offset, 120, 20, LabelHue, FormatAccessLevel(m.AccessLevel));
if (m.NetState != null)
{
AddLabelCropped(252, offset, 120, 20, GreenHue, "Online");
}
else
{
AddLabelCropped(252, offset, 120, 20, RedHue, "Offline");
}
AddButton(380, offset - 1, 0xFA5, 0xFA7, GetButtonID(5, i + 50));
++index;
}
if (index == 0)
{
AddLabel(12, 170, LabelHue, "The character list is empty.");
}
goto case AdminGumpPage.AccountDetails;
}
case AdminGumpPage.AccountDetails_Comments:
{
if (state is not Account a)
{
break;
}
AddHtml(10, 125, 400, 20, "Comments".Center(LabelColor32));
AddButtonLabeled(20, 150, GetButtonID(5, 4), "Add Comment");
var sb = ValueStringBuilder.Create();
if (a.Comments.Count == 0)
{
sb.Append("There are no comments for this account.");
}
for (var i = 0; i < a.Comments.Count; ++i)
{
if (i > 0)
{
sb.Append("<BR><BR>");
}
var c = a.Comments[i];
sb.Append($"[{c.AddedBy} on {c.LastModified}]<BR>{c.Content}");
}
AddHtml(20, 180, 380, 190, sb.ToString(), true, true);
sb.Dispose(); // Due to an analyzer bug, we can't use a using
goto case AdminGumpPage.AccountDetails;
}
case AdminGumpPage.AccountDetails_Tags:
{
if (state is not Account a)
{
break;
}
AddHtml(10, 125, 400, 20, "Tags".Center(LabelColor32));
AddButtonLabeled(20, 150, GetButtonID(5, 5), "Add Tag");
var sb = ValueStringBuilder.Create();
if (a.Tags.Count == 0)
{
sb.Append("There are no tags for this account.");
}
for (var i = 0; i < a.Tags.Count; ++i)
{
if (i > 0)
{
sb.Append("<BR>");
}
var tag = a.Tags[i];
sb.Append($"{tag.Name} = {tag.Value}");
}
AddHtml(20, 180, 380, 190, sb.ToString(), true, true);
sb.Dispose();
goto case AdminGumpPage.AccountDetails;
}
case AdminGumpPage.Firewall:
{
AddFirewallHeader();
if (m_List == null)
{
Firewall.ReadFirewallSet(firewallSet =>
{
list = new List<object>(firewallSet.Count);
list.AddRange(firewallSet);
});
m_List = list;
}
AddLabelCropped(12, 120, 358, 20, LabelHue, "IP Address");
if (listPage > 0)
{
AddButton(375, 122, 0x15E3, 0x15E7, GetButtonID(1, 0));
}
else
{
AddImage(375, 122, 0x25EA);
}
if ((listPage + 1) * 12 < m_List!.Count)
{
AddButton(392, 122, 0x15E1, 0x15E5, GetButtonID(1, 1));
}
else
{
AddImage(392, 122, 0x25E6);
}
if (m_List.Count == 0)
{
AddLabel(12, 140, LabelHue, "The firewall list is empty.");
}
else
{
var i = 0;
var index = listPage * 12;
foreach (var firewallEntry in m_List)
{
if (i >= 12)
{
break;
}
var offset = 140 + i++ * 20;
AddLabelCropped(12, offset, 358, 20, LabelHue, firewallEntry.ToString());
AddButton(380, offset - 1, 0xFA5, 0xFA7, GetButtonID(6, index++ + 4));
}
}
break;
}
case AdminGumpPage.FirewallInfo:
{
AddFirewallHeader();
if (state is not IFirewallEntry firewallEntry)
{
break;
}
AddHtml(10, 125, 400, 20, Html.Center($"{firewallEntry}", LabelColor32));
AddButtonLabeled(20, 150, GetButtonID(6, 3), "Remove");
AddHtml(10, 175, 400, 20, "Potentially Affected Accounts".Center(LabelColor32));
if (m_List == null)
{
using var blockedEntriesList = PooledRefList<Account>.Create();
foreach (var ia in Accounts.GetAccounts())
{
var acct = (Account)ia;
var loginList = acct.LoginIPs;
for (var i = 0; i < loginList.Length; ++i)
{
if (firewallEntry.IsBlocked(loginList[i]))
{
blockedEntriesList.Add(acct);
break;
}
}
}
blockedEntriesList.Sort(AccountComparer.Instance);
m_List = blockedEntriesList.ToList<Account, object>();
}
if (listPage > 0)
{
AddButton(375, 177, 0x15E3, 0x15E7, GetButtonID(1, 0));
}
else
{
AddImage(375, 177, 0x25EA);
}
if ((listPage + 1) * 12 < m_List.Count)
{
AddButton(392, 177, 0x15E1, 0x15E5, GetButtonID(1, 1));
}
else
{
AddImage(392, 177, 0x25E6);
}
if (m_List.Count == 0)
{
AddLabelCropped(12, 200, 398, 20, LabelHue, "No accounts found.");
}
for (int i = 0, index = listPage * 9;
i < 9 && index >= 0 && index < m_List.Count;
++i, ++index)
{
if (m_List[index] is not Account a)
{
continue;
}
var offset = 200 + i * 20;
GetAccountInfo(a, out var accessLevel, out var online);
AddLabelCropped(12, offset, 120, 20, LabelHue, a.Username);
AddLabelCropped(132, offset, 120, 20, LabelHue, FormatAccessLevel(accessLevel));
if (online)
{
AddLabelCropped(252, offset, 120, 20, GreenHue, "Online");
}
else if (a.Banned)
{
AddLabelCropped(252, offset, 120, 20, RedHue, "Banned");
}
else
{
AddLabelCropped(252, offset, 120, 20, RedHue, "Offline");
}
AddButton(380, offset - 1, 0xFA5, 0xFA7, GetButtonID(5, index + 56));
}
break;
}
}
}
public void AddPageButton(
int x, int y, int buttonID, string text, AdminGumpPage page,
params ReadOnlySpan<AdminGumpPage> subPages
)
{
var isSelection = m_PageType == page;
for (var i = 0; !isSelection && i < subPages.Length; ++i)
{
isSelection = m_PageType == subPages[i];
}
AddSelectedButton(x, y, buttonID, text, isSelection);
}
public void AddSelectedButton(int x, int y, int buttonID, string text, bool isSelection)
{
AddButton(x, y - 1, isSelection ? 4006 : 4005, 4007, buttonID);
AddHtml(x + 35, y, 200, 20, text.Color(isSelection ? SelectedColor32 : LabelColor32));
}
public void AddButtonLabeled(int x, int y, int buttonID, string text)
{
AddButton(x, y - 1, 4005, 4007, buttonID);
AddHtml(x + 35, y, 240, 20, text.Color(LabelColor32));
}
public void AddBlackAlpha(int x, int y, int width, int height)
{
AddImageTiled(x, y, width, height, 2624);
AddAlphaRegion(x, y, width, height);
}
public int GetButtonID(int type, int index) => 1 + index * 11 + type;
public static string FormatTimeSpan(TimeSpan ts) =>
$"{ts.Days:D2}:{ts.Hours % 24:D2}:{ts.Minutes % 60:D2}:{ts.Seconds % 60:D2}";
public static string FormatByteAmount(long totalBytes)
{
return totalBytes switch
{
> 1000000000 => $"{(double)totalBytes / 1073741824:F1} GB",
> 1000000 => $"{(double)totalBytes / 1048576:F1} MB",
> 1000 => $"{(double)totalBytes / 1024:F1} KB",
_ => $"{totalBytes} Bytes"
};
}
public static void Configure()
{
CommandSystem.Register("Admin", AccessLevel.Administrator, Admin_OnCommand);
}
[Usage("Admin"), Description(
"Opens an interface providing server information and administration features including client, account, and firewall management."
)]
public static void Admin_OnCommand(CommandEventArgs e)
{
e.Mobile.SendGump(new AdminGump(e.Mobile, AdminGumpPage.Clients));
}
public static int GetHueFor(Mobile m)
{
if (m == null)
{
return LabelHue;
}
switch (m.AccessLevel)
{
case AccessLevel.Owner:
case AccessLevel.Developer:
case AccessLevel.Administrator:
{
return 0x516;
}
case AccessLevel.Seer:
{
return 0x144;
}
case AccessLevel.GameMaster:
{
return 0x21;
}
case AccessLevel.Counselor:
{
return 0x2;
}
default:
{
if (m.Murderer)
{
return 0x21;
}
return m.Criminal ? 0x3B1 : 0x58;
}
}
}
public static string FormatAccessLevel(AccessLevel level)
{
var v = (int)level;
if (v >= 0 && v < m_AccessLevelStrings.Length)
{
return m_AccessLevelStrings[v];
}
return "Unknown";
}
public void AddTextField(int x, int y, int width, int height, int index)
{
AddBackground(x - 2, y - 2, width + 4, height + 4, 0x2486);
AddTextEntry(x + 2, y + 2, width - 4, height - 4, 0, index, "");
}
public void AddClientHeader()
{
AddTextField(200, 20, 200, 20, 0);
AddButtonLabeled(200, 50, GetButtonID(4, 0), "Search For Name");
AddButtonLabeled(200, 80, GetButtonID(4, 1), "Search For IP Address");
}
public void AddAccountHeader()
{
AddPage(1);
AddLabel(200, 20, LabelHue, "Name:");
AddTextField(250, 20, 150, 20, 0);
AddLabel(200, 50, LabelHue, "Pass:");
AddTextField(250, 50, 150, 20, 1);
AddButtonLabeled(200, 80, GetButtonID(5, 6), "Add");
AddButtonLabeled(290, 80, GetButtonID(5, 7), "Search");
AddButton(384, 84, 0x15E1, 0x15E5, 0, GumpButtonType.Page, 2);
AddPage(2);
AddButtonLabeled(200, 20, GetButtonID(5, 31), "View All: Inactive");
AddButtonLabeled(200, 40, GetButtonID(5, 32), "View All: Banned");
AddButtonLabeled(200, 60, GetButtonID(5, 26), "View All: Shared");
AddButtonLabeled(200, 80, GetButtonID(5, 30), "View All: Empty");
AddButton(384, 84, 0x15E1, 0x15E5, 0, GumpButtonType.Page, 1);
AddPage(0);
}
public void AddFirewallHeader()
{
AddTextField(200, 20, 200, 20, 0);
AddButtonLabeled(320, 50, GetButtonID(6, 0), "Search");
AddButtonLabeled(200, 50, GetButtonID(6, 1), "Add (Input)");
AddButtonLabeled(200, 80, GetButtonID(6, 2), "Add (Target)");
}
private static List<object> GetAllSharedAccounts()
{
var table = new Dictionary<IPAddress, List<Account>>();
foreach (Account acct in Accounts.GetAccounts())
{
var theirAddresses = acct.LoginIPs;
for (var i = 0; i < theirAddresses.Length; ++i)
{
var theirAddress = theirAddresses[i];
// This path is heavy for larger shards, so use optimized code
ref var accts = ref CollectionsMarshal.GetValueRefOrAddDefault(table, theirAddress, out var acctExists);
// If we don't have a list, create one
if (!acctExists)
{
accts = [];
}
accts.Add(acct);
}
}
var list = new List<object>();
// Lets find all the entries that have only one account
foreach (var kvp in table)
{
if (kvp.Value.Count > 1)
{
// Sort the accounts alphabetically
kvp.Value.Sort(AccountComparer.Instance);
// Can't avoid boxing because `m_List` in AdminGump is expecting List<object>
list.Add(kvp);
}
}
// Sort by highest accounts per IP first
list.Sort(SharedAccountDescendingComparer.Instance);
return list;
}
private static List<Account> GetSharedAccounts(IPAddress ipAddress)
{
var list = new List<Account>();
foreach (var account in Accounts.GetAccounts())
{
var acct = (Account)account;
var theirAddresses = acct.LoginIPs;
var contains = false;
for (var i = 0; !contains && i < theirAddresses.Length; ++i)
{
contains = ipAddress.Equals(theirAddresses[i]);
}
if (contains)
{
list.Add(acct);
}
}
list.Sort(AccountComparer.Instance);
return list;
}
private static List<Account> GetSharedAccounts(IPAddress[] ipAddresses)
{
var list = new List<Account>();
foreach (Account acct in Accounts.GetAccounts())
{
var theirAddresses = acct.LoginIPs;
var contains = false;
for (var i = 0; !contains && i < theirAddresses.Length; ++i)
{
var check = theirAddresses[i];
for (var j = 0; !contains && j < ipAddresses.Length; ++j)
{
contains = check.Equals(ipAddresses[j]);
}
}
if (contains)
{
list.Add(acct);
}
}
list.Sort(AccountComparer.Instance);
return list;
}
public static void BanShared_Callback(Mobile from, bool okay, Account a)
{
if (from.AccessLevel < AccessLevel.Administrator)
{
return;
}
string notice;
List<Account> list = null;
if (okay)
{
list = GetSharedAccounts(a.LoginIPs);
for (var i = 0; i < list.Count; ++i)
{
list[i].SetUnspecifiedBan(from);
list[i].Banned = true;
}
notice = "All addresses in the list have been banned.";
}
else
{
notice = "You have chosen not to ban all shared accounts.";
}
from.SendGump(new AdminGump(from, AdminGumpPage.AccountDetails_Access_ClientIPs, 0, null, notice, a));
if (okay)
{
from.SendGump(new BanDurationGump(list));
}
}
public static void AccountDelete_Callback(Mobile from, bool okay, Account a)
{
if (from.AccessLevel < AccessLevel.Administrator)
{
return;
}
if (okay)
{
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} deleting account {a.Username}"
);
a.Delete();
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Accounts,
0,
null,
$"{a.Username} : The account has been deleted."
)
);
}
else
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Information,
0,
null,
"You have chosen not to delete the account.",
a
)
);
}
}
public static void ResendGump_Callback(Mobile from, List<object> list, List<Account> rads, int page)
{
if (from.AccessLevel < AccessLevel.Administrator)
{
return;
}
from.SendGump(new AdminGump(from, AdminGumpPage.Accounts, page, list, null, rads));
}
public static void Marked_Callback(Mobile from, bool okay, bool ban, List<object> list, List<Account> rads, int page)
{
if (from.AccessLevel < AccessLevel.Administrator)
{
return;
}
if (okay)
{
if (!ban)
{
NetState.FlushAll();
}
for (var i = 0; i < rads.Count; ++i)
{
var acct = rads[i];
if (ban)
{
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} banning account {acct.Username}"
);
acct.SetUnspecifiedBan(from);
acct.Banned = true;
}
else
{
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} deleting account {acct.Username}"
);
acct.Delete();
rads.RemoveAt(i--);
list.Remove(acct);
}
}
from.SendGump(
new AdminNoticeGump(
$"You have {(ban ? "banned" : "deleted")} the account{(rads.Count == 1 ? "" : "s")}.",
() => ResendGump_Callback(from, list, rads, ban ? page : 0)
)
);
if (ban)
{
from.SendGump(new BanDurationGump(rads));
}
}
else
{
from.SendGump(
new AdminNoticeGump(
$"You have chosen not to {(ban ? "ban" : "delete")} the account{(rads.Count == 1 ? "" : "s")}.",
() => ResendGump_Callback(from, list, rads, page)
)
);
}
}
public static void FirewallShared_Callback(Mobile from, bool okay, Account a)
{
if (from.AccessLevel < AccessLevel.Administrator)
{
return;
}
string notice;
if (okay)
{
for (var i = 0; i < a.LoginIPs.Length; ++i)
{
Firewall.Add(new SingleIpFirewallEntry(a.LoginIPs[i]));
BanChannel.Report(a.LoginIPs[i], TimeSpan.Zero, BanReasons.Manual);
}
notice = "All addresses in the list have been firewalled.";
}
else
{
notice = "You have chosen not to firewall all addresses.";
}
from.SendGump(new AdminGump(from, AdminGumpPage.AccountDetails_Access_ClientIPs, 0, null, notice, a));
}
public static void Firewall_Callback(Mobile from, bool okay, Account a, object toFirewall)
{
if (from.AccessLevel < AccessLevel.Administrator)
{
return;
}
string notice;
if (okay)
{
var firewallEntry = Firewall.ToFirewallEntry(toFirewall);
Firewall.Add(firewallEntry);
if (firewallEntry.MinIpAddress == firewallEntry.MaxIpAddress)
{
BanChannel.Report(firewallEntry.MinIpAddress.ToIpAddress(), TimeSpan.Zero, BanReasons.Manual);
}
notice = $"{toFirewall} : Added to firewall.";
}
else
{
notice = "You have chosen not to firewall the address.";
}
from.SendGump(new AdminGump(from, AdminGumpPage.AccountDetails_Access_ClientIPs, 0, null, notice, a));
}
public static void RemoveLoginIP_Callback(Mobile from, bool okay, Account a, IPAddress ip)
{
if (from.AccessLevel < AccessLevel.Administrator)
{
return;
}
string notice;
if (okay)
{
var ips = a.LoginIPs;
if (ips.Length != 0 && Equals(ip, ips[0]) && AccountHandler.IPTable.ContainsKey(ips[0]))
{
--AccountHandler.IPTable[ip];
}
var newList = new List<IPAddress>(ips);
newList.Remove(ip);
a.LoginIPs = newList.ToArray();
notice = $"{ip} : Removed address.";
}
else
{
notice = "You have chosen not to remove the address.";
}
from.SendGump(new AdminGump(from, AdminGumpPage.AccountDetails_Access_ClientIPs, 0, null, notice, a));
}
public static void RemoveLoginIPs_Callback(Mobile from, bool okay, Account a)
{
if (from.AccessLevel < AccessLevel.Administrator)
{
return;
}
string notice;
if (okay)
{
var ips = a.LoginIPs;
if (ips.Length != 0 && AccountHandler.IPTable.ContainsKey(ips[0]))
{
--AccountHandler.IPTable[ips[0]];
}
a.LoginIPs = [];
notice = "All addresses in the list have been removed.";
}
else
{
notice = "You have chosen not to clear all addresses.";
}
from.SendGump(new AdminGump(from, AdminGumpPage.AccountDetails_Access_ClientIPs, 0, null, notice, a));
}
public override void OnResponse(NetState sender, in RelayInfo info)
{
var val = info.ButtonID - 1;
if (val < 0)
{
return;
}
var from = m_From;
if (from.AccessLevel < AccessLevel.Administrator)
{
return;
}
if (m_PageType == AdminGumpPage.Accounts)
{
if (m_State is List<Account> rads)
{
for (int i = 0, v = m_ListPage * 12; i < 12 && v < m_List.Count; ++i, ++v)
{
var acct = (Account)m_List[v];
if (!info.IsSwitched(v))
{
rads.Remove(acct);
}
else if (!rads.Contains(acct))
{
rads.Add(acct);
}
}
}
}
var type = val % 11;
var index = val / 11;
switch (type)
{
case 0:
{
AdminGumpPage page;
switch (index)
{
case 0:
{
page = AdminGumpPage.Information_General;
break;
}
case 1:
{
page = AdminGumpPage.Administer;
break;
}
case 2:
{
page = AdminGumpPage.Clients;
break;
}
case 3:
{
page = AdminGumpPage.Accounts;
break;
}
case 4:
{
page = AdminGumpPage.Firewall;
break;
}
case 5:
{
page = AdminGumpPage.Information_Perf;
break;
}
default:
{
return;
}
}
from.SendGump(new AdminGump(from, page));
break;
}
case 1:
{
switch (index)
{
case 0:
{
if (m_List != null && m_ListPage > 0)
{
from.SendGump(
new AdminGump(from, m_PageType, m_ListPage - 1, m_List, null, m_State)
);
}
break;
}
case 1:
{
if (m_List != null /*&& (m_ListPage + 1) * 12 < m_List.Count*/)
{
from.SendGump(
new AdminGump(from, m_PageType, m_ListPage + 1, m_List, null, m_State)
);
}
break;
}
}
break;
}
case 3:
{
string notice = null;
var page = AdminGumpPage.Administer;
if (index >= 500)
{
page = AdminGumpPage.Administer_Access_Lockdown;
}
else if (index >= 400)
{
page = AdminGumpPage.Administer_Commands;
}
else if (index >= 300)
{
page = AdminGumpPage.Administer_Access;
}
else if (index >= 200)
{
page = AdminGumpPage.Administer_Server;
}
else if (index >= 100)
{
page = AdminGumpPage.Administer_WorldBuilding;
}
switch (index)
{
case 0:
{
page = AdminGumpPage.Administer_WorldBuilding;
break;
}
case 1:
{
page = AdminGumpPage.Administer_Server;
break;
}
case 2:
{
page = AdminGumpPage.Administer_Access;
break;
}
case 3:
{
page = AdminGumpPage.Administer_Commands;
break;
}
case 101:
{
InvokeCommand("TelGen");
notice = "Teleporters have been generated.";
break;
}
case 102:
{
InvokeCommand("MoonGen");
notice = "Moongates have been generated.";
break;
}
case 104:
{
InvokeCommand("DoorGen");
notice = "Doors have been generated.";
break;
}
case 103:
{
GenerateSpawners();
notice = "Spawners have been generated.";
break;
}
case 105:
{
InvokeCommand("SignGen");
notice = "Signs have been generated.";
break;
}
case 106:
{
InvokeCommand("Decorate");
notice = "Decorations have been generated.";
break;
}
case 107:
{
InvokeCommand("GenChamps");
notice = "Champions have been generated.";
break;
}
case 108:
{
InvokeCommand("DecorateMag");
notice = "Magincia Ruins decorations have been generated.";
break;
}
case 109:
{
InvokeCommand("GenStealArties");
notice = "Stealable Artifacts have been generated.";
break;
}
case 110:
{
InvokeCommand("SHTelGen");
notice = "Solen Hive teleporters have been generated.";
break;
}
case 111:
{
InvokeCommand("SecretLocGen");
notice = "Secret Locations have been generated.";
break;
}
case 112:
{
InvokeCommand("GenLeverPuzzle");
InvokeCommand("GenGauntlet");
notice = "Doom has been generated.";
break;
}
case 113:
{
InvokeCommand("GenKhaldun");
notice = "Khaldun puzzles have been generated.";
break;
}
case 114:
{
// 101
InvokeCommand("TelGen");
// 102
InvokeCommand("MoonGen");
// 103
InvokeCommand("DoorGen");
// 104
GenerateSpawners();
// 105
InvokeCommand("SignGen");
// 106
InvokeCommand("Decorate");
// 107
InvokeCommand("GenChamps");
// 108
InvokeCommand("DecorateMag");
// 109
InvokeCommand("GenStealArties");
// 110
InvokeCommand("SHTelGen");
// 111
InvokeCommand("SecretLocGen");
// 112
InvokeCommand("GenLeverPuzzle");
InvokeCommand("GenGauntlet");
// 113
InvokeCommand("GenKhaldun");
notice = "All the above options have been generated at once.";
break;
}
case 200:
{
InvokeCommand("Save");
notice = "The world has been saved.";
break;
}
case 201:
{
Shutdown(false, true);
break;
}
case 202:
{
Shutdown(false, false);
break;
}
case 203:
{
Shutdown(true, true);
break;
}
case 204:
{
Shutdown(true, false);
break;
}
case 205: // shutdown with delay and save
{
var t = new ShutdownTimer(this);
t.Start();
break;
}
case 210:
case 211:
{
var text = info.GetTextEntry(0)?.Trim();
if (string.IsNullOrEmpty(text))
{
notice = "You must enter text to broadcast it.";
}
else
{
notice = "Your message has been broadcasted.";
InvokeCommand($"{(index == 210 ? "BC" : "SM")} {text}");
}
break;
}
case 300:
{
InvokeCommand("Kick");
notice = "Target the player to kick.";
break;
}
case 301:
{
InvokeCommand("Ban");
notice = "Target the player to ban.";
break;
}
case 302:
{
InvokeCommand("Firewall");
notice = "Target the player to firewall.";
break;
}
case 303:
{
page = AdminGumpPage.Administer_Access_Lockdown;
break;
}
case 310:
{
InvokeCommand("Set AccessLevel Player");
notice = "Target the player to change their access level. (Player)";
break;
}
case 311:
{
InvokeCommand("Set AccessLevel Counselor");
notice = "Target the player to change their access level. (Counselor)";
break;
}
case 312:
{
InvokeCommand("Set AccessLevel GameMaster");
notice = "Target the player to change their access level. (Game Master)";
break;
}
case 313:
{
InvokeCommand("Set AccessLevel Seer");
notice = "Target the player to change their access level. (Seer)";
break;
}
case 314:
{
if (from.AccessLevel > AccessLevel.Administrator)
{
InvokeCommand("Set AccessLevel Administrator");
notice = "Target the player to change their access level. (Administrator)";
}
break;
}
case 315:
{
if (from.AccessLevel > AccessLevel.Developer)
{
InvokeCommand("Set AccessLevel Developer");
notice = "Target the player to change their access level. (Developer)";
}
break;
}
case 316:
{
if (from.AccessLevel >= AccessLevel.Owner)
{
InvokeCommand("Set AccessLevel Owner");
notice = "Target the player to change their access level. (Owner)";
}
break;
}
case 400:
{
notice = "Enter search terms to add objects.";
break;
}
case 401:
{
InvokeCommand("Remove");
notice = "Target the item or mobile to remove.";
break;
}
case 402:
{
InvokeCommand("Dupe");
notice = "Target the item to dupe.";
break;
}
case 403:
{
InvokeCommand("DupeInBag");
notice = "Target the item to dupe. The item will be duped at it's current location.";
break;
}
case 404:
{
InvokeCommand("Props");
notice = "Target the item or mobile to inspect.";
break;
}
case 405:
{
InvokeCommand("Skills");
notice = "Target a mobile to view their skills.";
break;
}
case 406:
{
InvokeCommand("Set Blessed False");
notice = "Target the mobile to make mortal.";
break;
}
case 407:
{
InvokeCommand("Set Blessed True");
notice = "Target the mobile to make immortal.";
break;
}
case 408:
{
InvokeCommand("Set Squelched True");
notice = "Target the mobile to squelch.";
break;
}
case 409:
{
InvokeCommand("Set Squelched False");
notice = "Target the mobile to unsquelch.";
break;
}
case 410:
{
InvokeCommand("Set Frozen True");
notice = "Target the mobile to freeze.";
break;
}
case 411:
{
InvokeCommand("Set Frozen False");
notice = "Target the mobile to unfreeze.";
break;
}
case 412:
{
InvokeCommand("Set Hidden True");
notice = "Target the mobile to hide.";
break;
}
case 413:
{
InvokeCommand("Set Hidden False");
notice = "Target the mobile to unhide.";
break;
}
case 414:
{
InvokeCommand("Kill");
notice = "Target the mobile to kill.";
break;
}
case 415:
{
InvokeCommand("Resurrect");
notice = "Target the mobile to resurrect.";
break;
}
case 416:
{
InvokeCommand("Move");
notice = "Target the item or mobile to move.";
break;
}
case 417:
{
InvokeCommand("Wipe");
notice = "Target bounding points.";
break;
}
case 418:
{
InvokeCommand("Tele");
notice = "Choose your destination.";
break;
}
case 419:
{
InvokeCommand("Multi Tele");
notice = "Choose your destination.";
break;
}
case 500:
case 501:
case 502:
case 503:
case 504:
{
AccountHandler.LockdownLevel = (AccessLevel)(index - 500);
if (AccountHandler.LockdownLevel > AccessLevel.Player)
{
notice = "The lockdown level has been changed.";
}
else
{
notice = "The server is now accessible to everyone.";
}
break;
}
case 510:
{
var level = AccountHandler.LockdownLevel;
if (level > AccessLevel.Player)
{
var count = 0;
foreach (var ns in NetState.Instances)
{
var a = ns.Account;
if (a == null)
{
continue;
}
var hasAccess = false;
if (a.AccessLevel >= level)
{
hasAccess = true;
}
else
{
for (var j = 0; !hasAccess && j < a.Length; ++j)
{
var m = a[j];
if (m?.AccessLevel >= level)
{
hasAccess = true;
}
}
}
if (!hasAccess)
{
ns.Disconnect("Server has been locked down.");
++count;
}
}
if (count == 0)
{
notice = "Nobody without access was found to disconnect.";
}
else
{
notice = $"Number of players disconnected: {count}";
}
}
else
{
notice = "The server is not currently locked down.";
}
break;
}
}
from.SendGump(new AdminGump(from, page, 0, null, notice));
switch (index)
{
case 400:
{
InvokeCommand("Add");
break;
}
}
break;
}
case 4:
{
switch (index)
{
case 0:
case 1:
{
var forName = index == 0;
var results = new List<NetState>();
var match = info.GetTextEntry(0)?.Trim().ToLower();
string notice = null;
if (string.IsNullOrEmpty(match))
{
notice = $"You must enter {(forName ? "a name" : "an ip address")} to search.";
}
else
{
foreach (var ns in NetState.Instances)
{
bool isMatch;
if (forName)
{
var m = ns.Mobile;
var a = ns.Account;
isMatch = m?.Name.InsensitiveContains(match) == true
|| a?.Username.InsensitiveContains(match) == true;
}
else
{
isMatch = ns.ToString().ContainsOrdinal(match);
}
if (isMatch)
{
results.Add(ns);
}
}
results.Sort(NetStateComparer.Instance);
}
if (results.Count == 1)
{
var ns = results[0];
var state = ns.Mobile ?? (object)ns.Account;
if (state is Mobile)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.ClientInfo,
0,
null,
"One match found.",
state
)
);
}
else if (state is Account)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Information,
0,
null,
"One match found.",
state
)
);
}
else
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Clients,
0,
results.ToList<object>(),
"One match found."
)
);
}
}
else
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Clients,
0,
results.ToList<object>(),
notice ?? (results.Count == 0 ? "Nothing matched your search terms." : null)
)
);
}
break;
}
default:
{
index -= 2;
if (index < m_List?.Count)
{
if (m_List[index] is not NetState ns)
{
break;
}
var m = ns.Mobile;
var a = ns.Account as Account;
if (m != null)
{
from.SendGump(new AdminGump(from, AdminGumpPage.ClientInfo, 0, null, null, m));
}
else if (a != null)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Information,
0,
null,
null,
a
)
);
}
}
break;
}
}
break;
}
case 5:
{
switch (index)
{
case 0:
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Information,
0,
null,
null,
m_State
)
);
break;
}
case 1:
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Characters,
0,
null,
null,
m_State
)
);
break;
}
case 2:
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Comments,
0,
null,
null,
m_State
)
);
break;
}
case 3:
{
from.SendGump(
new AdminGump(from, AdminGumpPage.AccountDetails_Tags, 0, null, null, m_State)
);
break;
}
case 13:
{
from.SendGump(
new AdminGump(from, AdminGumpPage.AccountDetails_Access, 0, null, null, m_State)
);
break;
}
case 14:
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Access_ClientIPs,
0,
null,
null,
m_State
)
);
break;
}
case 4:
{
from.Prompt = new AddCommentPrompt(m_State as Account);
from.SendMessage("Enter the new account comment.");
break;
}
case 5:
{
from.Prompt = new AddTagNamePrompt(m_State as Account);
from.SendMessage("Enter the new tag name.");
break;
}
case 6:
{
var un = info.GetTextEntry(0)?.Trim();
var pw = info.GetTextEntry(1)?.Trim();
Account dispAccount = null;
string notice;
if (string.IsNullOrEmpty(un))
{
notice = "You must enter a username to add an account.";
}
else if (string.IsNullOrEmpty(pw))
{
notice = "You must enter a password to add an account.";
}
else
{
var account = Accounts.GetAccount(un);
if (account != null)
{
notice = "There is already an account with that username.";
}
else
{
dispAccount = new Account(un, pw);
notice = $"{un} : Account added.";
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} adding new account: {un}"
);
}
}
from.SendGump(
new AdminGump(
from,
dispAccount != null ? AdminGumpPage.AccountDetails_Information : m_PageType,
m_ListPage,
m_List,
notice,
dispAccount ?? m_State
)
);
break;
}
case 7:
{
List<IAccount> results;
var match = info.GetTextEntry(0)?.Trim().ToLower();
if (string.IsNullOrEmpty(match))
{
results = Accounts.GetAccounts().ToList();
results.Sort(AccountComparer.Instance);
}
else
{
results = [];
foreach (var acct in Accounts.GetAccounts())
{
if (acct.Username.InsensitiveContains(match))
{
results.Add(acct);
}
}
results.Sort(AccountComparer.Instance);
}
if (results.Count == 1)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Information,
0,
null,
"One match found.",
results[0]
)
);
}
else
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Accounts,
0,
results.ToList<object>(),
results.Count == 0 ? "Nothing matched your search terms." : null,
new List<object>()
)
);
}
break;
}
case 8:
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_ChangePassword,
0,
null,
null,
m_State
)
);
break;
}
case 9:
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_ChangeAccess,
0,
null,
null,
m_State
)
);
break;
}
case 10:
case 11:
{
if (m_State is not Account a)
{
break;
}
a.SetUnspecifiedBan(from);
a.Banned = index == 10;
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} {a.Username} account {(a.Banned ? "banning" : "unbanning")}"
);
from.SendGump(
new AdminGump(
from,
m_PageType,
m_ListPage,
m_List,
$"The account has been {(a.Banned ? "banned" : "unbanned")}.",
m_State
)
);
if (index == 10)
{
from.SendGump(new BanDurationGump(a));
}
break;
}
case 12:
{
if (m_State is not Account a)
{
break;
}
var password = info.GetTextEntry(0)?.Trim();
var confirm = info.GetTextEntry(1)?.Trim();
string notice;
var page = AdminGumpPage.AccountDetails_ChangePassword;
if (string.IsNullOrEmpty(password))
{
notice = "You must enter the password.";
}
else if (confirm != password)
{
notice =
"You must confirm the password. That field must precisely match the password field.";
}
else
{
notice = "The password has been changed.";
PasswordWorker.SetPassword(a, password, null);
page = AdminGumpPage.AccountDetails_Information;
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} changing password of account {a.Username}"
);
}
from.SendGump(new AdminGump(from, page, 0, null, notice, m_State));
break;
}
case 16: // view shared
{
if (m_State is not Account a)
{
break;
}
var list = GetSharedAccounts(a.LoginIPs);
if (list.Count > 1 || list.Count == 1 && !list.Contains(a))
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Accounts,
0,
list.ToList<object>(),
null,
new List<object>()
)
);
}
else if (a.LoginIPs.Length > 0)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Access_ClientIPs,
0,
null,
"There are no other accounts which share an address with this one.",
m_State
)
);
}
else
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Access_ClientIPs,
0,
null,
"This account has not yet been accessed.",
m_State
)
);
}
break;
}
case 17: // ban shared
{
if (m_State is not Account a)
{
break;
}
var list = GetSharedAccounts(a.LoginIPs);
if (list.Count > 0)
{
using var sb = ValueStringBuilder.Create();
sb.Append("You are about to ban ");
sb.Append(list.Count);
sb.Append(list.Count != 1 ? "accounts." : "account.");
sb.Append(" Do you wish to continue?");
for (var i = 0; i < list.Count; ++i)
{
sb.Append("<br>- ");
sb.Append(list[i].Username);
}
from.SendGump(
new WarningGump(
sb.ToString(),
420,
400,
okay => BanShared_Callback(from, okay, a)
)
);
}
else if (a.LoginIPs.Length > 0)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Access_ClientIPs,
0,
null,
"There are no accounts which share an address with this one.",
m_State
)
);
}
else
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Access_ClientIPs,
0,
null,
"This account has not yet been accessed.",
m_State
)
);
}
break;
}
case 18: // firewall all
{
if (m_State is not Account a)
{
break;
}
if (a.LoginIPs.Length > 0)
{
from.SendGump(
new WarningGump(
$"You are about to firewall {a.LoginIPs.Length} address{(a.LoginIPs.Length != 1 ? "s" : "")}. Do you wish to continue?",
420,
400,
okay => FirewallShared_Callback(from, okay, a)
)
);
}
else
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Access_ClientIPs,
0,
null,
"This account has not yet been accessed.",
m_State
)
);
}
break;
}
case 20: // Change access level
case 21:
case 22:
case 23:
case 24:
{
if (m_State is not Account a)
{
break;
}
var newLevel = index switch
{
21 => AccessLevel.Counselor,
22 => AccessLevel.GameMaster,
23 => AccessLevel.Seer,
24 => AccessLevel.Administrator,
33 => AccessLevel.Developer,
34 => AccessLevel.Owner,
_ => AccessLevel.Player // 20
};
if (newLevel < from.AccessLevel || from.AccessLevel == AccessLevel.Owner)
{
a.AccessLevel = newLevel;
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} changing access level of account {a.Username} to {a.AccessLevel}"
);
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Information,
0,
null,
"The access level has been changed.",
m_State
)
);
}
break;
}
case 25:
{
if (m_State is not Account a)
{
break;
}
from.SendGump(
new WarningGump(
$"<center>Account of {a.Username}</center><br>You are about to <em><basefont color=red>permanently delete</basefont></em> the account. Likewise, all characters on the account will be deleted, including equipped, inventory, and banked items. Any houses tied to the account will be demolished.<br><br>Do you wish to continue?",
420,
280,
okay => AccountDelete_Callback(from, okay, a)
)
);
break;
}
case 26: // View all shared accounts
{
from.SendGump(new AdminGump(from, AdminGumpPage.Accounts_Shared));
break;
}
case 27: // Ban marked
{
var list = m_List;
if (list == null || m_State is not List<Account> rads)
{
break;
}
if (rads.Count > 0)
{
from.SendGump(
new WarningGump(
$"You are about to ban {rads.Count} marked account{(rads.Count == 1 ? "" : "s")}. Be cautioned, the only way to reverse this is by hand--manually unbanning each account.<br><br>Do you wish to continue?",
420,
280,
okay => Marked_Callback(from, okay, true, list, rads, m_ListPage)
)
);
}
else
{
from.SendGump(
new AdminNoticeGump(
"You have not yet marked any accounts. Place a check mark next to the accounts you wish to ban and then try again.",
() => ResendGump_Callback(from, list, rads, m_ListPage)
)
);
}
break;
}
case 28: // Delete marked
{
var list = m_List;
if (list == null || m_State is not List<Account> rads)
{
break;
}
if (rads.Count > 0)
{
from.SendGump(
new WarningGump(
$"You are about to <em><basefont color=red>permanently delete</basefont></em> {rads.Count} marked account{(rads.Count == 1 ? "" : "s")}. Likewise, all characters on the account{(rads.Count == 1 ? "" : "s")} will be deleted, including equipped, inventory, and banked items. Any houses tied to the account{(rads.Count == 1 ? "" : "s")} will be demolished.<br><br>Do you wish to continue?",
420,
280,
okay => Marked_Callback(from, okay, false, list, rads, m_ListPage)
)
);
}
else
{
from.SendGump(
new AdminNoticeGump(
"You have not yet marked any accounts. Place a check mark next to the accounts you wish to ban and then try again.",
() => ResendGump_Callback(from, list, rads, m_ListPage)
)
);
}
break;
}
case 29: // Mark all
{
if (m_List == null || m_State is not List<object>)
{
break;
}
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Accounts,
m_ListPage,
m_List,
null,
m_List.ToList()
)
);
break;
}
case 30: // View all empty accounts
{
var results = new List<object>();
foreach (Account acct in Accounts.GetAccounts())
{
var empty = true;
for (var i = 0; empty && i < acct.Length; ++i)
{
empty = acct[i] == null;
}
if (empty)
{
results.Add(acct);
}
}
if (results.Count == 1)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Information,
0,
null,
"One match found.",
results[0]
)
);
}
else
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Accounts,
0,
results,
results.Count == 0 ? "Nothing matched your search terms." : null,
new List<object>()
)
);
}
break;
}
case 31: // View all inactive accounts
{
var results = new List<object>();
foreach (Account acct in Accounts.GetAccounts())
{
if (acct.Inactive)
{
results.Add(acct);
}
}
if (results.Count == 1)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Information,
0,
null,
"One match found.",
results[0]
)
);
}
else
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Accounts,
0,
results,
results.Count == 0 ? "Nothing matched your search terms." : null,
new List<object>()
)
);
}
break;
}
case 32: // View all banned accounts
{
var results = new List<object>();
foreach (Account acct in Accounts.GetAccounts())
{
if (acct.Banned)
{
results.Add(acct);
}
}
if (results.Count == 1)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Information,
0,
null,
"One match found.",
results[0]
)
);
}
else
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Accounts,
0,
results,
results.Count == 0 ? "Nothing matched your search terms." : null,
new List<object>()
)
);
}
break;
}
case 33: // Change access level (extended)
case 34:
{
goto case 20;
}
case 35: // Unmark house owners
{
var list = m_List;
var rads = m_State as List<Account>;
if (list == null || rads == null)
{
break;
}
var newRads = new List<Account>();
foreach (var acct in rads)
{
var hasHouse = false;
for (var i = 0; i < acct.Length && !hasHouse; ++i)
{
if (acct[i] != null && BaseHouse.HasHouse(acct[i]))
{
hasHouse = true;
}
}
if (!hasHouse)
{
newRads.Add(acct);
}
}
from.SendGump(
new AdminGump(from, AdminGumpPage.Accounts, m_ListPage, m_List, null, newRads)
);
break;
}
case 36: // Clear login addresses
{
if (m_State is not Account a)
{
break;
}
var ips = a.LoginIPs;
if (ips.Length == 0)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Access_ClientIPs,
0,
null,
"This account has not yet been accessed.",
m_State
)
);
}
else
{
from.SendGump(
new WarningGump(
$"You are about to clear the address list for account {a} containing {ips.Length} {(ips.Length == 1 ? "entry" : "entries")}. Do you wish to continue?",
420,
280,
okay => RemoveLoginIPs_Callback(from, okay, a)
)
);
}
break;
}
default:
{
index -= 50;
if (m_State is Account a && index >= 0 && index < a.Length)
{
var m = a[index];
if (m != null)
{
from.SendGump(new AdminGump(from, AdminGumpPage.ClientInfo, 0, null, null, m));
}
}
else
{
index -= 6;
if (m_List != null && index >= 0 && index < m_List.Count)
{
if (m_List[index] is Account)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Information,
0,
null,
null,
m_List[index]
)
);
}
else if (m_List[index] is KeyValuePair<IPAddress, List<Account>> kvp)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Accounts,
0,
kvp.Value.ToList<object>(),
null,
new List<object>()
)
);
}
}
}
break;
}
}
break;
}
case 6:
{
switch (index)
{
case 0:
{
var match = info.GetTextEntry(0)?.Trim();
string notice = null;
var results = new List<object>();
if (string.IsNullOrEmpty(match))
{
notice = "You must enter an IP to search.";
}
else
{
Firewall.ReadFirewallSet(firewallSet =>
{
foreach (var check in firewallSet)
{
var checkStr = check.ToString();
if (checkStr.ContainsOrdinal(match))
{
results.Add(check);
}
}
});
}
if (results.Count == 1)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.FirewallInfo,
0,
null,
"One match found.",
results[0]
)
);
}
else if (results.Count > 1)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Firewall,
0,
results,
$"Search results for : {match}",
m_State
)
);
}
else
{
from.SendGump(
new AdminGump(
from,
m_PageType,
m_ListPage,
m_List,
notice ?? "Nothing matched your search terms.",
m_State
)
);
}
break;
}
case 1:
{
var text = info.GetTextEntry(0)?.Trim();
if (string.IsNullOrEmpty(text))
{
from.SendGump(
new AdminGump(
from,
m_PageType,
m_ListPage,
m_List,
"You must enter an address or CIDR to add.",
m_State
)
);
}
else
{
IFirewallEntry firewallEntry;
try
{
firewallEntry = Firewall.ToFirewallEntry(text);
}
catch
{
from.SendGump(
new AdminGump(
from,
m_PageType,
m_ListPage,
m_List,
"That is not a valid address or CIDR.",
m_State
)
);
break;
}
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} firewalling {firewallEntry}"
);
Firewall.Add(firewallEntry);
if (firewallEntry.MinIpAddress == firewallEntry.MaxIpAddress)
{
BanChannel.Report(
firewallEntry.MinIpAddress.ToIpAddress(),
TimeSpan.Zero,
BanReasons.Manual
);
}
from.SendGump(
new AdminGump(
from,
AdminGumpPage.FirewallInfo,
0,
null,
$"{firewallEntry} : Added to firewall.",
firewallEntry
)
);
}
break;
}
case 2:
{
InvokeCommand("Firewall");
from.SendGump(
new AdminGump(
from,
m_PageType,
m_ListPage,
m_List,
"Target the player to firewall.",
m_State
)
);
break;
}
case 3:
{
if (m_State is IFirewallEntry)
{
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} removing {m_State} from firewall list"
);
Firewall.Remove(m_State as IFirewallEntry);
if (m_State is IFirewallEntry fe && fe.MinIpAddress == fe.MaxIpAddress)
{
BanChannel.Retract(fe.MinIpAddress.ToIpAddress());
}
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Firewall,
0,
null,
$"{m_State} : Removed from firewall."
)
);
}
break;
}
default:
{
index -= 4;
if (index < m_List?.Count)
{
from.SendGump(
new AdminGump(from, AdminGumpPage.FirewallInfo, 0, null, null, m_List[index])
);
}
break;
}
}
break;
}
case 7:
{
if (m_State is not Mobile m ||
m.AccessLevel > from.AccessLevel || m.Account?.AccessLevel > from.AccessLevel)
{
break;
}
string notice = null;
var sendGump = true;
switch (index)
{
case 0:
{
var map = m.Map;
var loc = m.Location;
if (map == null || map == Map.Internal)
{
map = m.LogoutMap;
loc = m.LogoutLocation;
}
if (map != null && map != Map.Internal)
{
from.MoveToWorld(loc, map);
notice = "You have been teleported to their location.";
}
break;
}
case 1:
{
m.MoveToWorld(from.Location, from.Map);
notice = "They have been teleported to your location.";
break;
}
case 2:
{
var ns = m.NetState;
if (ns != null)
{
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} kicking {CommandLogging.Format(m)}"
);
ns.Disconnect($"Kicked by {from}.");
notice = "They have been kicked.";
}
else
{
notice = "They are already disconnected.";
}
break;
}
case 3:
{
if (m.Account is Account a)
{
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} banning {CommandLogging.Format(m)}"
);
a.Banned = true;
var ns = m.NetState;
ns?.Disconnect($"Banned by {from}.");
notice = "They have been banned.";
}
break;
}
case 6:
{
Properties.SetValue(from, m, "Blessed", "False");
notice = "They are now mortal.";
break;
}
case 7:
{
Properties.SetValue(from, m, "Blessed", "True");
notice = "They are now immortal.";
break;
}
case 8:
{
Properties.SetValue(from, m, "Squelched", "True");
notice = "They are now squelched.";
break;
}
case 9:
{
Properties.SetValue(from, m, "Squelched", "False");
notice = "They are now unsquelched.";
break;
}
case 10:
{
Properties.SetValue(from, m, "Hidden", "True");
notice = "They are now hidden.";
break;
}
case 11:
{
Properties.SetValue(from, m, "Hidden", "False");
notice = "They are now unhidden.";
break;
}
case 12:
{
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} killing {CommandLogging.Format(m)}"
);
m.Kill();
notice = "They have been killed.";
break;
}
case 13:
{
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} resurrecting {CommandLogging.Format(m)}"
);
m.Resurrect();
notice = "They have been resurrected.";
break;
}
case 14:
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Information,
0,
null,
null,
m.Account
)
);
sendGump = false;
break;
}
case 15:
{
var reason = info.GetTextEntry(1)?.Trim();
if (string.IsNullOrWhiteSpace(reason))
{
reason = "";
}
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} jailing {CommandLogging.Format(m)} - Reason: {reason}"
);
InvokeCommand($"Jail {m.Name} \"{reason}\"");
notice = $"Player has been sent to jail. Reason: {reason}";
break;
}
case 16:
{
CommandLogging.WriteLine(
from,
$"{from.AccessLevel} {CommandLogging.Format(from)} unjailing {CommandLogging.Format(m)}"
);
InvokeCommand($"Unjail {m.Name}");
notice = "Player has been unjailed.";
break;
}
}
if (sendGump)
{
from.SendGump(new AdminGump(from, AdminGumpPage.ClientInfo, 0, null, notice, m_State));
}
switch (index)
{
case 3:
{
if (m.Account is Account a)
{
from.SendGump(new BanDurationGump(a));
}
break;
}
case 4:
{
from.SendGump(new PropertiesGump(from, m));
break;
}
case 5:
{
from.SendGump(new SkillsGump(from, m));
break;
}
}
break;
}
case 8:
{
if (index < m_List?.Count)
{
if (m_State is not Account a)
{
break;
}
if (m_PageType == AdminGumpPage.AccountDetails_Access_ClientIPs)
{
from.SendGump(
new WarningGump(
$"You are about to firewall {m_List[index]}. All connection attempts from a matching IP will be refused. Are you sure?",
420,
280,
okay => Firewall_Callback(from, okay, a, m_List[index])
)
);
}
}
break;
}
case 9:
{
if (index < m_List?.Count)
{
if (m_PageType == AdminGumpPage.AccountDetails_Access_ClientIPs)
{
var obj = m_List[index];
if (obj is not IPAddress ip)
{
break;
}
if (m_State is not Account a)
{
break;
}
var list = GetSharedAccounts(ip);
if (list.Count > 1 || list.Count == 1 && !list.Contains(a))
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.Accounts,
0,
list.ToList<object>(),
null,
new List<object>()
)
);
}
else
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Access_ClientIPs,
0,
null,
"There are no other accounts which share that address.",
a
)
);
}
}
}
break;
}
case 10:
{
if (index < m_List?.Count)
{
if (m_PageType == AdminGumpPage.AccountDetails_Access_ClientIPs)
{
var ip = m_List[index] as IPAddress;
if (ip == null)
{
break;
}
if (m_State is not Account a)
{
break;
}
from.SendGump(
new WarningGump(
$"You are about to remove address {ip} from account {a}. Do you wish to continue?",
420,
280,
okay => RemoveLoginIP_Callback(from, okay, a, ip)
)
);
}
}
break;
}
}
}
private void GenerateSpawners()
{
var folder = Core.SA ? "post-uoml" : "uoml";
var availableMaps = ExpansionInfo.CoreExpansion.MapSelectionFlags;
if (Core.SA && availableMaps.Includes(MapSelectionFlags.TerMur))
{
InvokeCommand("GenerateSpawners Data/Spawns/post-uoml/termur/**.json");
InvokeCommand("GenerateSpawners Data/Spawns/shared/termur/**.json");
}
if (availableMaps.Includes(MapSelectionFlags.Malas))
{
InvokeCommand($"GenerateSpawners Data/Spawns/{folder}/malas/**.json");
InvokeCommand("GenerateSpawners Data/Spawns/shared/malas/**.json");
}
if (availableMaps.Includes(MapSelectionFlags.Tokuno))
{
InvokeCommand($"GenerateSpawners Data/Spawns/{folder}/tokuno/**.json");
InvokeCommand("GenerateSpawners Data/Spawns/shared/tokuno/**.json");
}
if (availableMaps.Includes(MapSelectionFlags.Ilshenar))
{
InvokeCommand($"GenerateSpawners Data/Spawns/{folder}/ilshenar/**.json");
InvokeCommand("GenerateSpawners Data/Spawns/shared/ilshenar/**.json");
}
if (availableMaps.Includes(MapSelectionFlags.Trammel))
{
InvokeCommand($"GenerateSpawners Data/Spawns/{folder}/trammel/**.json");
InvokeCommand("GenerateSpawners Data/Spawns/shared/trammel/**.json");
}
if (availableMaps.Includes(MapSelectionFlags.Felucca))
{
InvokeCommand($"GenerateSpawners Data/Spawns/{folder}/felucca/**.json");
InvokeCommand("GenerateSpawners Data/Spawns/shared/felucca/**.json");
}
}
private void Shutdown(bool restart, bool save)
{
CommandLogging.WriteLine(
m_From,
$"{m_From.AccessLevel} {CommandLogging.Format(m_From)} shutting down server (Restart: {restart}) (Save: {save})"
);
if (save)
{
InvokeCommand("Save");
}
// Kill the server on a different thread otherwise we will dead lock
ThreadPool.QueueUserWorkItem(
_ =>
{
World.WaitForWriteCompletion();
Core.Kill(restart);
}
);
}
private void InvokeCommand(string c)
{
CommandSystem.Handle(m_From, $"{CommandSystem.Prefix}{c}");
}
public static void GetAccountInfo(IAccount a, out AccessLevel accessLevel, out bool online)
{
accessLevel = a.AccessLevel;
online = false;
for (var j = 0; j < a.Length; ++j)
{
var check = a[j];
if (check == null)
{
continue;
}
if (check.AccessLevel > accessLevel)
{
accessLevel = check.AccessLevel;
}
if (check.NetState != null)
{
online = true;
}
}
}
private static void FilterAccess(List<object> list, Mobile from)
{
if (list == null || list.Count == 0)
{
return;
}
for (var i = list.Count - 1; i >= 0; i--)
{
var obj = list[i];
if (obj is Account acc && acc.AccessLevel > from.AccessLevel ||
obj is Mobile mob && mob.AccessLevel > from.AccessLevel ||
obj is NetState ns &&
(ns.Mobile?.AccessLevel > from.AccessLevel || ns.Account?.AccessLevel > from.AccessLevel))
{
list.RemoveAt(i);
}
}
}
private class SharedAccountDescendingComparer : IComparer<object>
{
public static readonly IComparer<object> Instance = new SharedAccountDescendingComparer();
public int Compare(object x, object y)
{
if (x is not KeyValuePair<IPAddress, List<Account>> a)
{
return 1;
}
if (y is not KeyValuePair<IPAddress, List<Account>> b)
{
return -1;
}
return b.Value.Count - a.Value.Count;
}
}
private class AddCommentPrompt : Prompt
{
private readonly Account m_Account;
public AddCommentPrompt(Account acct) => m_Account = acct;
public override void OnCancel(Mobile from)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Comments,
0,
null,
"Request to add comment was canceled.",
m_Account
)
);
}
public override void OnResponse(Mobile from, string text)
{
if (m_Account != null)
{
m_Account.Comments.Add(new AccountComment(from.RawName, text));
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Comments,
0,
null,
"Comment added.",
m_Account
)
);
}
}
}
private class AddTagNamePrompt : Prompt
{
private readonly Account m_Account;
public AddTagNamePrompt(Account acct) => m_Account = acct;
public override void OnCancel(Mobile from)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Tags,
0,
null,
"Request to add tag was canceled.",
m_Account
)
);
}
public override void OnResponse(Mobile from, string text)
{
from.Prompt = new AddTagValuePrompt(m_Account, text);
from.SendMessage("Enter the new tag value.");
}
}
private class AddTagValuePrompt : Prompt
{
private readonly Account m_Account;
private readonly string m_Name;
public AddTagValuePrompt(Account acct, string name)
{
m_Account = acct;
m_Name = name;
}
public override void OnCancel(Mobile from)
{
from.SendGump(
new AdminGump(
from,
AdminGumpPage.AccountDetails_Tags,
0,
null,
"Request to add tag was canceled.",
m_Account
)
);
}
public override void OnResponse(Mobile from, string text)
{
if (m_Account != null)
{
m_Account.AddTag(m_Name, text);
from.SendGump(new AdminGump(from, AdminGumpPage.AccountDetails_Tags, 0, null, "Tag added.", m_Account));
}
}
}
private class NetStateComparer : IComparer<NetState>
{
public static readonly IComparer<NetState> Instance = new NetStateComparer();
public int Compare(NetState x, NetState y)
{
if (x == null && y == null)
{
return 0;
}
if (x == null)
{
return -1;
}
if (y == null)
{
return 1;
}
var aMob = x.Mobile;
var bMob = y.Mobile;
if (aMob == null && bMob == null)
{
return 0;
}
if (aMob == null)
{
return 1;
}
if (bMob == null)
{
return -1;
}
if (aMob.AccessLevel > bMob.AccessLevel)
{
return -1;
}
return aMob.AccessLevel < bMob.AccessLevel ? 1 : aMob.Name.InsensitiveCompare(bMob.Name);
}
}
private class AccountComparer : IComparer<IAccount>
{
public static readonly IComparer<IAccount> Instance = new AccountComparer();
public int Compare(IAccount x, IAccount y)
{
if (x == null && y == null)
{
return 0;
}
if (x == null)
{
return -1;
}
if (y == null)
{
return 1;
}
GetAccountInfo(x, out var aLevel, out var aOnline);
GetAccountInfo(y, out var bLevel, out var bOnline);
if (aOnline && !bOnline)
{
return -1;
}
if (bOnline && !aOnline)
{
return 1;
}
if (aLevel > bLevel)
{
return -1;
}
return aLevel < bLevel ? 1 : x.Username.InsensitiveCompare(y.Username);
}
}
private class AdminNoticeGump : StaticNoticeGump<AdminNoticeGump>
{
public override int Width => 420;
public override int Height => 280;
public override string Content { get; }
public AdminNoticeGump(string content, Action callback) : base(callback) => Content = content;
}
public class ShutdownTimer : Timer
{
private readonly AdminGump _adminGump;
public ShutdownTimer(AdminGump gump) : base(TimeSpan.Zero, TimeSpan.Zero, 8) =>
_adminGump = gump;
protected override void OnTick()
{
if (Index >= 7)
{
AutoSave.SavesEnabled = false;
_adminGump.Shutdown(false, true);
return;
}
ReadOnlySpan<int> times = [15, 10, 5, 4, 3, 2, 1, 0];
var time = times[Index];
_adminGump.m_From.SendMessage(
$"The shard will shutdown in {time} minute{(time == 1 ? "s" : "")} for maintenance."
);
Interval = TimeSpan.FromMinutes(time - times[Index + 1]);
}
}
}
}