## Why The shard owner, on a Starlink CGNAT address, was blocked by the imported reputation blocklist. The cause was not CrowdSec. The address was a literal line in `ip-blocklist.txt`, so `BlocklistFilter` denied it at accept and then promoted it — and clearing the CrowdSec decision could not fix it either, because the file entry re-reports within `promoteSuppression` of every reconnect attempt. This is structural, not a one-off. Reputation feeds list shared consumer address space constantly: on CGNAT one public address fronts many subscribers **at the same time**, so a single abusive customer gets the address listed and everyone else behind it is blocked with them. Where leases rotate, a listing says little about whoever holds the address now. Around 1,000 Starlink addresses sit in the current list. So exemptions go where they cost nothing, and escalation is driven by what a connection actually does. ## Generator — `tools/Export-IpBlocklist.ps1` `-AllowlistFile` takes multiple paths, subtracted from the merged set before the output is written. Defaults to every `ip-allowlist*.txt` beside the output, merged into one allow set: - `ip-allowlist.txt` — operator exemptions, created once and **never rewritten** - `ip-allowlist-<name>.txt` — a carve-out you built, regenerable and copyable between shards **Subtraction is range-correct.** An allowlisted address inside a blocked CIDR splits that CIDR around the hole rather than being silently ignored. This also fixes `-ExcludeAnonymizers`, which parsed CIDR entries into `$anonCidr` and then only ever subtracted singles. **No carve-out ships.** A carve-out names a real network, and which ones a shard should exempt depends on where its players actually are — so publishing one would make that policy call for every shard and put a specific provider's address space in the repo. The script builds them on request instead: ```powershell .\Export-IpBlocklist.ps1 -AddCarveout starlink -Asn 14593 ``` Carve-outs are **discovered, not configured**: every `ip-allowlist*.txt` beside the output is subtracted, by the generator and by the shard, so a file an admin adds needs no config edit and no code change. Each carries an `asn=` marker in its header, which is how `-RefreshCarveouts` rebuilds it without the script keeping a list of anyone's networks; a hand-written allowlist has no marker and is never rewritten. Prefixes come from **announcements, not ownership records**, because registry data disagrees with what is actually routed and silently caps result sets: ARIN whois returns at most 256 rows and gives per-customer /24s, and `206.83.96.0/19` reads as APNIC in RDAP even though `206.83.96/21` is announced by Starlink. Editing an allowlist bypasses `-MinInterval`, so a just-added exemption isn't indistinguishable from the allowlist not working. A Starlink carve-out, if you build one, costs **~4,300 IPs + ~144 CIDRs of 4.2M (0.10%)**. ## Allowlists **`FileAllowlist`** reads the same files the generator subtracts, so an operator entry means "leave this address alone" for real. Subtraction alone only covers being *blocked*; behavioural detections never consult the blocklist, so without this a carve-out was quietly routed around — one scanner behind a shared address was enough to get everyone behind it contributed and firewalled, with nothing in the shard's own config explaining why. Reading the files also means an entry applies on the next reload rather than the next regeneration, which is what matters when someone is complaining now. **`LoginAllowlist`** is earned by authenticating, with a 90-day TTL because an address that logged in years ago is a stranger. Its own store rather than `Account.LoginIPs`, which has no timestamps and cannot be backfilled. An entry is evidence rather than a licence: 10 suppressed contributions in an hour revokes it, and a fresh login forgives the tally. Both are consulted **only after the blocklist has already matched**, so a normal accept pays nothing for them and the accept gate stays allowlist-free. `BanExemptions` combines them behind `BanChannel.IsExempt` and suppresses escalation only — every local defence still applies. Two limits, both deliberate and documented in the class: `LoginAllowlist` **cannot bootstrap** (an entry is only earned by getting in, so it never repairs an existing false positive), and it is weakest on rotating CGNAT. That is why `FileAllowlist` is the fix for those, and why it is manual. ## Behavioural detection | Reason | Trigger | |---|---| | `silent-connect` | Reaped after 5s having sent **zero bytes** | | `invalid-seed` | Opened with a zero seed | | `foreign-protocol` | Positively identified as HTTP, TLS or SSH | **`ForeignProtocol` inverts the test.** Asking "is this a good UO client?" cannot work: `LoginEncryption.ClientDecrypt` is a byte-for-byte stream XOR, so a legitimate client with encryption enabled when the shard expects none sends a structurally perfect connection whose payload is noise. "Speaks HTTP" is safe where "unreadable" is not — however misconfigured a UO client is, it never sends `GET / HTTP/1.1`. Nothing assumes arrival framing. TCP has no message boundaries, so a rule of the form "these bytes must arrive together" is broken by construction and drops real players on poor links. A prefix match with too few bytes to confirm waits for more. A four-byte seed can legitimately spell `GET ` (the address 71.69.84.32) or `0x16 0x03 0x0?` (22.3.x.x), so confirmation requires the request line to continue in printable ASCII or an actual ClientHello inside a plausible record — a real client's fifth byte is a packet id (`0x80`, `0x91`, `0xEF`), none of them printable, so those collisions fall through. Everything is keyed on **bytes-received rather than elapsed time**. A connection that sent something and ran out of time is far more likely a slow link than an attack, and banning those produces the worst failure mode available: the player retries, trips the rate limiter, and compounds a bad connection into hours of being firewalled off. ## `AutoDenylist` A short-lived local hold (15m) on behavioural detections, as `IConnectionFilter` + `IBanReporter` over one store so the engine detection sites never reach into content. This closes the gap where a flood pays for a socket, buffer and `NetState` slot per connection while waiting for the OS bouncer — the verdicts that matter most are reachable only *after* reading bytes — and it is the entire defence on a shard running no bouncer, which is the default config. Not persisted: a holding pen that survives restarts is a ban without a ban's review. Cost: one dictionary lookup on a usually-empty dict per accept. ## `BanReasons` Centralises the reason slugs. `IsBehavioral` is an **opt-in** set, not "everything except manual", so a future reason escalates normally instead of silently inheriting an exemption or entering a local denylist. This caught a real bug during review: the first cut of the exemption swallowed `manual` admin bans (`Commands.cs`, three sites in `AdminGump`) for any allowlisted address. ## Fixes found in review - **`BanConfiguration.Settings` was null until `Configure()` ran**, while the reap path dereferences it every `Slice()`. A harness driving `NetState.Slice()` directly hit an NRE that presented as flaky because it depended on whether an earlier test had already called `Configure()` — which is why it failed on some CI platforms and not others. Now starts at the record's defaults, with idempotency tracked by a flag; this also removes the same latent NRE from the pre-existing rate-limit path. - **`-AllowlistFile` was typed `[string]`** while documented and used as a list, so passing two paths would have collapsed them into one string. ## Layout and docs Content network code moves out of `Misc/` into `UOContent/Network/`, one concern per folder — `AutoDenylist/`, `Blocklist/`, `CrowdSec/`, `Firewall/`, `LoginAllowlist/`, `Packets/`. **Namespaces are untouched**, so these are pure file moves (git tracks all 16 as renames). `dev-docs/ip-bans-and-allowlists.md` documents the subsystem, leading with the operator process for unblocking a player — including the three things that look sufficient and are not: deleting the CrowdSec decision alone, editing `ip-blocklist.txt` by hand, and `cscli allowlists` alone. `.gitignore` covers the new config files. ## Testing Build clean. **Server.Tests 810 passed**, **UOContent.Tests 637 passed**, zero warnings. This branch adds 38 tests; the rest of the delta is main's, since this is rebased on current `main`. New coverage: TTL boundary and renewal, private-address exclusion, manual-ban-never-exempt, unopted-reason-never-exempt, strike revocation, quiet-window reset, login forgiveness, file-allowlist CIDR coverage, file-allowlist not spending the earned list's strikes, denylist expiry-on-read, cap enforcement, lapsed-entry reclaim, HTTP/TLS/SSH identification, seed-collision fall-through, and encrypted-login-is-not-foreign. Generator verified end-to-end against live feeds: a clean run ships no carve-out, `-AddCarveout starlink -Asn 14593` fetches and collapses 213 prefixes to 115 ranges in 0.1s over 4.2M entries, `-RefreshCarveouts` rediscovers it by its `asn=` marker, a hand-written allowlist is left untouched, and deleting a carve-out drops it rather than having it rewritten. CIDR splitting verified exhaustively: a single-IP hole in a /24 leaves exactly 255 of 256 addresses blocked. ## Operator note Existing installs are unaffected until the generator next runs, which creates `ip-allowlist.txt` and nothing else. To unblock someone: add the address to that file and delete any live CrowdSec decision — the existing ban outlives the config change. The shard picks the entry up on its next reload, so re-running the generator is optional. A shard whose players are on CGNAT (satellite, mobile, or an ISP short on IPv4) will likely also want `-AddCarveout`; see `dev-docs/ip-bans-and-allowlists.md`. ## Also included: a latent CI failure this PR surfaced `fix(tests): serialize test classes that rent through STArrayPool` touches a property-list test file that has nothing to do with this feature. It is here because it was failing macOS CI, and it is trivially cherry-pickable out if you would rather it went to `main` on its own — **which may be the better call, since it is failing `main` today.** CI has since gone green with it applied. `STArrayPool` is single-threaded by design and its bucket cache is a plain `static`, not `[ThreadStatic]`, with a check-then-act initialize in `Return()`: ```csharp var cacheBuckets = _cacheBuckets ?? InitializeBuckets(); ``` Two threads both see null, both initialize, and the loser trips `Debug.Assert(_cacheBuckets is null)`. Anything renting from it has to stay off parallel test threads — which is what the `DisableParallelization` collections are for. - `ObjectPropertyListReentrancyTests` and `ObjectPropertyListNestedBuildTests` (added in #2555) build property lists, which rent the interpolation buffer, but were not in the sequential collection — unlike `PropertyListInvalidationDuringBuildTests` in the same file. This is a **latent failure already on `main`**; it is timing-dependent, so it shows on some platforms and not others. - `AutoDenylistTests` (added here) has the same exposure: its cap tests reach `AutoDenylist.Sweep`, which rents a `PooledRefList` without `mt`. The blocklist tests need no marking because `BlocklistSnapshot.Build` asks for the `mt` pool explicitly. No production change — `STArrayPool` is the right pool on the game loop, where both `Sweep` and the property list actually run. ## Deliberately not included Waiting for a fragmented four-byte seed at `AwaitingSeed`. It looked like a bug but the disconnect is a deliberate defence: only pre-0xEF clients reach it (0xEF goes through `HandlePacket`, which already waits for its 21 bytes), and waiting converts an instant drop into a full 5s slot hold for a client sending one or two bytes, or a loris dribbling a byte every few seconds. Against a fixed 4096-entry `MaxConnections` table that trades capacity that matters for a fragmentation case a reconnect already fixes.
4357 lines
182 KiB
C#
4357 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.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.";
|
|
a.SetPassword(password);
|
|
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]);
|
|
}
|
|
}
|
|
}
|
|
}
|