This reverts ceec3bc4. Two independent reasons, either sufficient.
The rendering regression. The slice deleted TextRenderGlStateScope, which
saved GL_MULTISAMPLE and GL_SAMPLE_ALPHA_TO_COVERAGE on entry, disabled them
for the text pass, and restored them on exit (TextRenderGlStateScope.cs:111-112
and 153-154 at the parent commit). Its replacement bakes that state into the
text pipeline but nothing restores it, and GlGpuPassEncoder.Dispose does not
either. Every world renderer is still raw GL at this point in the campaign, so
from the first UI frame onward the world drew with multisampling disabled.
The offline pixel gate caught it: 1,791 of 563,200 compared pixels differed,
0.318% against a 0.001 threshold. The commit message attributed this to
wall-clock-driven ambient animation shifting phase, and committed through the
failure. That explanation does not survive its own control: capturing twice at
the reverted-to commit differs by 19 pixels and twice at the slice's own commit
by 8, while base-versus-head differs by 1,791 - a 224x gap that no shared-noise
source explains. An amplified difference image settles it visually: the changed
pixels are the silhouette edges of every tree, building and rock, with terrain
interiors, water and the entire UI untouched. That is the signature of losing
edge antialiasing, not of animated sprites.
This is the exact failure mode two existing memory notes already warn about -
a mid-frame renderer must set every GL state it uses rather than inherit it,
and issue #52's lesson that a rendering migration must audit per-pass GL state
before declaring itself done.
The scope. The brief was three small leaf renderers plus additive frame-
lifecycle wiring, roughly ten files. The commit changed 334 files with 3,665
insertions and 3,845 deletions, including 323 public-to-internal visibility
conversions across the App assembly, 55 test files, two retired conformance
tests, and a self-described temporary escape hatch for bridging raw-GL viewport
textures. Even without the regression, that is not separable into the part
worth keeping and the part worth dropping.
Reverting rather than patching because the good work here - the RHI frame
lifecycle wiring and a genuine render-state-cache staleness fix - is small
enough to redo cleanly against a tightened spec, while untangling it from 300+
files of unrelated churn is not.
Post-revert: Release build clean, App suite back to 3,843 passed / 3 skipped,
offline pixel gate passing at 19 differing pixels.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
666 lines
25 KiB
C#
666 lines
25 KiB
C#
using AcDream.Core.Physics;
|
|
using AcDream.Core.Ui;
|
|
using AcDream.Core.Social;
|
|
using AcDream.UI.Abstractions;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
/// <summary>
|
|
/// Application-layer executor for typed retail client commands. Chat panels
|
|
/// remain backend- and network-agnostic; this controller owns the boundary
|
|
/// between verified command behavior and live session/UI services.
|
|
/// </summary>
|
|
public sealed class ClientCommandController
|
|
{
|
|
public sealed record Bindings(
|
|
Action TeleportToLifestone,
|
|
Action TeleportToMarketplace,
|
|
Action TeleportToPkArena,
|
|
Action TeleportToPkLiteArena,
|
|
Action TeleportToHouse,
|
|
Action TeleportToMansion,
|
|
Action QueryAge,
|
|
Action QueryBirth,
|
|
Action ToggleFrameRate,
|
|
Action ToggleUiLock,
|
|
Action<string> ShowSystemMessage,
|
|
Action<uint> ShowWeenieError,
|
|
Func<uint?> PlayerPublicWeenieBitfield,
|
|
Func<string> ClientVersion,
|
|
Func<Position?> CurrentPosition,
|
|
Func<Position?> LastOutsideCorpsePosition,
|
|
Action<string, Action<bool>> ShowConfirmation,
|
|
Action Suicide,
|
|
Action<bool> ClearChat,
|
|
Action<string> SaveUi,
|
|
Action<string> LoadUi,
|
|
Action SaveAutoUi,
|
|
Action LoadAutoUi,
|
|
Func<bool> IsAway,
|
|
Action<bool> SetAway,
|
|
Action<string> SetAwayMessage,
|
|
Func<bool> AcceptLootPermits,
|
|
Action<bool> SetAcceptLootPermits,
|
|
Action DisplayConsent,
|
|
Action ClearConsent,
|
|
Action<string> RemoveConsent,
|
|
Action<string> SendEmote,
|
|
FriendsState Friends,
|
|
Action<string> AddFriend,
|
|
Action<uint> RemoveFriend,
|
|
Action ClearFriends,
|
|
Action RequestLegacyFriends,
|
|
SquelchState Squelch,
|
|
Action<bool, uint, string, uint> ModifyCharacterSquelch,
|
|
Action<bool, string> ModifyAccountSquelch,
|
|
Action<bool, uint> ModifyGlobalSquelch,
|
|
Func<string?> LastTeller,
|
|
Action ClearDesiredComponents,
|
|
Func<bool> HasOpenVendor,
|
|
Action<uint?, uint> FillComponentBuyList);
|
|
|
|
private readonly Bindings _bindings;
|
|
|
|
public ClientCommandController(Bindings bindings)
|
|
{
|
|
_bindings = bindings ?? throw new ArgumentNullException(nameof(bindings));
|
|
}
|
|
|
|
public void Execute(ExecuteClientCommandCmd command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
switch (command.Command)
|
|
{
|
|
// ClientCommunicationSystem::DoLifestone @ 0x0056FC70.
|
|
case ClientCommandId.LifestoneRecall:
|
|
_bindings.TeleportToLifestone();
|
|
break;
|
|
// ClientCommunicationSystem::DoMarketplace @ 0x0056FCE0.
|
|
case ClientCommandId.MarketplaceRecall:
|
|
_bindings.TeleportToMarketplace();
|
|
break;
|
|
// DoPKArena @ 0x005788D0 gates on ACCWeenieObject::IsPK.
|
|
case ClientCommandId.PkArenaRecall:
|
|
if (HasPlayerFlag(EntityCollisionFlags.IsPK) == false)
|
|
_bindings.ShowWeenieError(0x055Fu);
|
|
else
|
|
_bindings.TeleportToPkArena();
|
|
break;
|
|
// DoPKLArena @ 0x005789D0 gates on ACCWeenieObject::IsPKLite.
|
|
case ClientCommandId.PkLiteArenaRecall:
|
|
if (HasPlayerFlag(EntityCollisionFlags.IsPKLite) == false)
|
|
_bindings.ShowWeenieError(0x0560u);
|
|
else
|
|
_bindings.TeleportToPkLiteArena();
|
|
break;
|
|
case ClientCommandId.HouseRecall:
|
|
_bindings.TeleportToHouse();
|
|
break;
|
|
case ClientCommandId.MansionRecall:
|
|
_bindings.TeleportToMansion();
|
|
break;
|
|
case ClientCommandId.QueryAge:
|
|
_bindings.QueryAge();
|
|
break;
|
|
case ClientCommandId.QueryBirth:
|
|
_bindings.QueryBirth();
|
|
break;
|
|
// DoFrameRate @ 0x005707D0 toggles the display flag; it does
|
|
// not print a one-shot FPS sample into chat.
|
|
case ClientCommandId.ToggleFrameRate:
|
|
_bindings.ToggleFrameRate();
|
|
break;
|
|
// DoLockUI @ 0x005703B0 toggles PlayerModule::LockUI and
|
|
// broadcasts the new state to every UI element.
|
|
case ClientCommandId.ToggleUiLock:
|
|
_bindings.ToggleUiLock();
|
|
break;
|
|
case ClientCommandId.ShowVersion:
|
|
_bindings.ShowSystemMessage($"Client version {_bindings.ClientVersion()}");
|
|
break;
|
|
case ClientCommandId.ShowLocation:
|
|
Position? position = _bindings.CurrentPosition();
|
|
_bindings.ShowSystemMessage(position is { ObjCellId: not 0u }
|
|
? $"Your location is: {RetailPositionFormatter.Format(position.Value)}"
|
|
: "Not in valid cell!");
|
|
break;
|
|
case ClientCommandId.ShowLastCorpseLocation:
|
|
Position? corpse = _bindings.LastOutsideCorpsePosition();
|
|
string? coordinates = corpse is null
|
|
? null
|
|
: RetailPositionFormatter.FormatOutdoorCell(corpse.Value.ObjCellId);
|
|
_bindings.ShowSystemMessage(coordinates is null
|
|
? "We're sorry, but we have no record of your last outside corpse location."
|
|
: $"The last time you died outside, your corpse was located at ({coordinates}).");
|
|
break;
|
|
// ClientCommunicationSystem::DoDie @ 0x00580050 and
|
|
// DieDialogCallback @ 0x0057BA70.
|
|
case ClientCommandId.Die:
|
|
_bindings.ShowConfirmation(
|
|
"Do you really want to kill your character? You may drop items and accrue a vitae penalty.",
|
|
accepted =>
|
|
{
|
|
if (accepted)
|
|
_bindings.Suicide();
|
|
});
|
|
break;
|
|
case ClientCommandId.ClearChat:
|
|
_bindings.ClearChat(FirstArgument(command.Arguments)
|
|
.Equals("all", StringComparison.OrdinalIgnoreCase));
|
|
break;
|
|
case ClientCommandId.SaveUi:
|
|
ExecuteUiProfile(command.Arguments, save: true);
|
|
break;
|
|
case ClientCommandId.LoadUi:
|
|
ExecuteUiProfile(command.Arguments, save: false);
|
|
break;
|
|
case ClientCommandId.SaveAutoUi:
|
|
if (RequireNoArguments(command.Arguments, "/saveautoui"))
|
|
_bindings.SaveAutoUi();
|
|
break;
|
|
case ClientCommandId.LoadAutoUi:
|
|
if (RequireNoArguments(command.Arguments, "/loadautoui"))
|
|
_bindings.LoadAutoUi();
|
|
break;
|
|
case ClientCommandId.Away:
|
|
ExecuteAway(command.Arguments);
|
|
break;
|
|
case ClientCommandId.Consent:
|
|
ExecuteConsent(command.Arguments);
|
|
break;
|
|
case ClientCommandId.Emote:
|
|
if (!string.IsNullOrWhiteSpace(command.Arguments))
|
|
_bindings.SendEmote(command.Arguments.Trim());
|
|
break;
|
|
case ClientCommandId.ListEmotes:
|
|
_bindings.ShowSystemMessage(StandardEmotes);
|
|
break;
|
|
case ClientCommandId.Friends:
|
|
ExecuteFriends(command.Arguments);
|
|
break;
|
|
case ClientCommandId.FriendsAdd:
|
|
AddFriend(command.Arguments);
|
|
break;
|
|
case ClientCommandId.FriendsRemove:
|
|
RemoveFriend(command.Arguments);
|
|
break;
|
|
case ClientCommandId.Squelch:
|
|
ExecuteSquelch(command.Arguments, add: true);
|
|
break;
|
|
case ClientCommandId.Unsquelch:
|
|
ExecuteSquelch(command.Arguments, add: false);
|
|
break;
|
|
case ClientCommandId.Filter:
|
|
ExecuteGlobalFilter(command.Arguments, add: true);
|
|
break;
|
|
case ClientCommandId.Unfilter:
|
|
ExecuteGlobalFilter(command.Arguments, add: false);
|
|
break;
|
|
case ClientCommandId.ListMessageTypes:
|
|
_bindings.ShowSystemMessage(
|
|
"Squelch channels are as follows:\n "
|
|
+ string.Join(", ", MessageTypes.Values));
|
|
break;
|
|
case ClientCommandId.FillComponents:
|
|
ExecuteFillComponents(command.Arguments);
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(command), command.Command, "Unknown retail client command.");
|
|
}
|
|
}
|
|
|
|
private void ExecuteUiProfile(string arguments, bool save)
|
|
{
|
|
string[] parts = SplitArguments(arguments);
|
|
string command = save ? "saveui" : "loadui";
|
|
if (parts.Length > 1)
|
|
{
|
|
_bindings.ShowSystemMessage($"Please use @help {command} for proper usage.");
|
|
return;
|
|
}
|
|
|
|
string name = parts.Length == 0 ? string.Empty : parts[0];
|
|
if (name.Length > 16)
|
|
{
|
|
_bindings.ShowSystemMessage("The file name must be 16 characters or less.");
|
|
return;
|
|
}
|
|
|
|
if (save) _bindings.SaveUi(name);
|
|
else _bindings.LoadUi(name);
|
|
}
|
|
|
|
private bool RequireNoArguments(string arguments, string usage)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(arguments)) return true;
|
|
_bindings.ShowSystemMessage($"Usage: {usage}");
|
|
return false;
|
|
}
|
|
|
|
private void ExecuteAway(string arguments)
|
|
{
|
|
string first = FirstArgument(arguments);
|
|
if (first.Length == 0 || first.Equals("on", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (!_bindings.IsAway()) _bindings.SetAway(true);
|
|
return;
|
|
}
|
|
|
|
if (first.Equals("off", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (_bindings.IsAway()) _bindings.SetAway(false);
|
|
return;
|
|
}
|
|
|
|
if (first.Equals("msg", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string message = RemainderAfterFirstArgument(arguments).Trim(' ');
|
|
if (message.Length > 191) message = message[..191];
|
|
if (message.Length > 0 && !message.Contains('\n')) message += "\n";
|
|
_bindings.SetAwayMessage(message);
|
|
_bindings.ShowSystemMessage(message.Length == 0
|
|
? "New AFK message set: I am currently away from the keyboard."
|
|
: $"New AFK message set: {message}");
|
|
return;
|
|
}
|
|
|
|
_bindings.ShowSystemMessage(AwayHelp);
|
|
}
|
|
|
|
private void ExecuteConsent(string arguments)
|
|
{
|
|
string first = FirstArgument(arguments);
|
|
if (first.Equals("on", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (!_bindings.AcceptLootPermits()) _bindings.SetAcceptLootPermits(true);
|
|
_bindings.ShowSystemMessage(
|
|
"You can now accept corpse looting permissions from other players.");
|
|
}
|
|
else if (first.Equals("off", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (_bindings.AcceptLootPermits()) _bindings.SetAcceptLootPermits(false);
|
|
_bindings.ShowSystemMessage(
|
|
"You are no longer accepting corpse looting permissions from other players.");
|
|
}
|
|
else if (first.Equals("who", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_bindings.DisplayConsent();
|
|
}
|
|
else if (first.Equals("clear", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_bindings.ClearConsent();
|
|
}
|
|
else if (first.Equals("remove", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string name = RemainderAfterFirstArgument(arguments).Trim();
|
|
if (name.Length == 0)
|
|
_bindings.ShowSystemMessage(
|
|
"Please specify a person to remove from your consent list.");
|
|
else
|
|
_bindings.RemoveConsent(name);
|
|
}
|
|
else
|
|
{
|
|
_bindings.ShowSystemMessage("Please specify a valid consent command.");
|
|
}
|
|
}
|
|
|
|
private void ExecuteFriends(string arguments)
|
|
{
|
|
string operation = FirstArgument(arguments);
|
|
if (operation.Length == 0)
|
|
{
|
|
DisplayFriends(onlineOnly: false);
|
|
return;
|
|
}
|
|
|
|
if (operation.Equals("online", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
DisplayFriends(onlineOnly: true);
|
|
return;
|
|
}
|
|
|
|
string remainder = RemainderAfterFirstArgument(arguments);
|
|
if (operation.Equals("add", StringComparison.OrdinalIgnoreCase))
|
|
AddFriend(remainder);
|
|
else if (operation.Equals("remove", StringComparison.OrdinalIgnoreCase))
|
|
RemoveFriend(remainder);
|
|
else if (operation.Equals("old", StringComparison.OrdinalIgnoreCase)
|
|
&& string.IsNullOrWhiteSpace(remainder))
|
|
_bindings.RequestLegacyFriends();
|
|
else
|
|
_bindings.ShowSystemMessage("Invalid friends command specified.");
|
|
}
|
|
|
|
private void AddFriend(string arguments)
|
|
{
|
|
string name = arguments.Trim();
|
|
if (name.Length == 0)
|
|
{
|
|
_bindings.ShowSystemMessage(
|
|
"You must specify the name of the friend you wish to add.");
|
|
return;
|
|
}
|
|
|
|
if (_bindings.Friends.Snapshot().Count >= 50)
|
|
{
|
|
_bindings.ShowWeenieError(0x0561u);
|
|
return;
|
|
}
|
|
|
|
_bindings.AddFriend(name);
|
|
}
|
|
|
|
private void RemoveFriend(string arguments)
|
|
{
|
|
string name = arguments.Trim();
|
|
if (name.Length == 0)
|
|
{
|
|
_bindings.ShowSystemMessage(
|
|
"You must specify the name of the friend you wish to remove.");
|
|
return;
|
|
}
|
|
|
|
if (name.Equals("-all", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_bindings.ClearFriends();
|
|
_bindings.Friends.Clear();
|
|
_bindings.ShowSystemMessage("Your friends list has been cleared.\n");
|
|
return;
|
|
}
|
|
|
|
FriendEntry? friend = _bindings.Friends.Snapshot().FirstOrDefault(
|
|
entry => entry.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
|
if (friend is null)
|
|
_bindings.ShowWeenieError(0x0563u);
|
|
else
|
|
_bindings.RemoveFriend(friend.Id);
|
|
}
|
|
|
|
private void DisplayFriends(bool onlineOnly)
|
|
{
|
|
IReadOnlyList<FriendEntry> entries = _bindings.Friends.Snapshot();
|
|
if (entries.Count == 0)
|
|
{
|
|
_bindings.ShowSystemMessage("Your friends list is empty!\n");
|
|
return;
|
|
}
|
|
|
|
var lines = entries
|
|
.Where(entry => !onlineOnly || entry.Online)
|
|
.Select(entry => $" {entry.Name}{(entry.Online ? " (Online)" : string.Empty)}")
|
|
.ToArray();
|
|
_bindings.ShowSystemMessage(lines.Length == 0
|
|
? "Your friends:\n You have no friends that are online.\n"
|
|
: "Your friends:\n" + string.Join("\n", lines) + "\n");
|
|
}
|
|
|
|
private void ExecuteSquelch(string arguments, bool add)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(arguments))
|
|
{
|
|
DisplayCharacterSquelches();
|
|
return;
|
|
}
|
|
|
|
if (!TryParseSquelch(arguments, out SquelchArguments parsed, out string error))
|
|
{
|
|
_bindings.ShowSystemMessage(error);
|
|
return;
|
|
}
|
|
|
|
if (parsed.AccountWide)
|
|
_bindings.ModifyAccountSquelch(add, parsed.Name);
|
|
else
|
|
_bindings.ModifyCharacterSquelch(add, 0u, parsed.Name, parsed.MessageType);
|
|
}
|
|
|
|
private void ExecuteGlobalFilter(string arguments, bool add)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(arguments))
|
|
{
|
|
DisplayGlobalFilters();
|
|
return;
|
|
}
|
|
|
|
string[] parts = SplitArguments(arguments);
|
|
if (parts.Length != 1 || !parts[0].StartsWith('-'))
|
|
{
|
|
_bindings.ShowSystemMessage("Incorrect usage, use @help for proper usage.");
|
|
return;
|
|
}
|
|
|
|
if (!TryGetMessageType(parts[0][1..], out uint type) || type == 1u)
|
|
{
|
|
_bindings.ShowSystemMessage("You must specify a valid message type.");
|
|
return;
|
|
}
|
|
|
|
_bindings.ModifyGlobalSquelch(add, type);
|
|
}
|
|
|
|
private bool TryParseSquelch(
|
|
string arguments,
|
|
out SquelchArguments parsed,
|
|
out string error)
|
|
{
|
|
parsed = default;
|
|
error = string.Empty;
|
|
string[] parts = SplitArguments(arguments);
|
|
bool account = false;
|
|
uint messageType = 1u;
|
|
string? replyName = null;
|
|
int index = 0;
|
|
for (; index < parts.Length && parts[index].StartsWith('-'); index++)
|
|
{
|
|
string option = parts[index][1..];
|
|
if (option.Equals("account", StringComparison.OrdinalIgnoreCase))
|
|
account = true;
|
|
else if (option.Equals("reply", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
replyName = _bindings.LastTeller();
|
|
if (string.IsNullOrWhiteSpace(replyName))
|
|
{
|
|
error = "A player must @tell you before you can use the -reply option.";
|
|
return false;
|
|
}
|
|
}
|
|
else if (!TryGetMessageType(option, out messageType))
|
|
{
|
|
error = $"\"{option}\" is not a valid squelch category.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
string name = replyName ?? string.Join(' ', parts.Skip(index));
|
|
if (name.Length == 0)
|
|
{
|
|
error = "You have not specified a squelch target.";
|
|
return false;
|
|
}
|
|
|
|
parsed = new SquelchArguments(account, messageType, name);
|
|
return true;
|
|
}
|
|
|
|
private void DisplayCharacterSquelches()
|
|
{
|
|
SquelchDatabase database = _bindings.Squelch.Snapshot();
|
|
var lines = database.Characters.Values
|
|
.Where(info => info.MessageTypes.Count > 0)
|
|
.Select(FormatSquelchInfo)
|
|
.ToArray();
|
|
_bindings.ShowSystemMessage(
|
|
"(account) denotes a character whose account has also been squelched.\n"
|
|
+ "Format: Name : List of squelched message types.\n--------\n"
|
|
+ (lines.Length == 0 ? "none\n" : string.Join("\n", lines) + "\n"));
|
|
}
|
|
|
|
private void DisplayGlobalFilters()
|
|
{
|
|
SquelchInfo global = _bindings.Squelch.Snapshot().Global;
|
|
string list = global.MessageTypes.Count == 0
|
|
? "none"
|
|
: FormatMessageTypes(global.MessageTypes);
|
|
_bindings.ShowSystemMessage(
|
|
"The following types of messages are currently being filtered globally:\n"
|
|
+ list + "\n(For a list of filter options, type @help filter)\n");
|
|
}
|
|
|
|
private static string FormatSquelchInfo(SquelchInfo info) =>
|
|
$"Name: {info.Name}{(info.AccountWide ? " (account) " : " ")}"
|
|
+ FormatMessageTypes(info.MessageTypes);
|
|
|
|
private static string FormatMessageTypes(IReadOnlySet<uint> types)
|
|
{
|
|
if (types.Contains(1u)) return "All message types";
|
|
return string.Join(", ", MessageTypes
|
|
.Where(pair => pair.Key != 1u && types.Contains(pair.Key))
|
|
.Select(pair => pair.Value));
|
|
}
|
|
|
|
private void ExecuteFillComponents(string arguments)
|
|
{
|
|
string[] parts = SplitArguments(arguments);
|
|
if (parts.Length > 2)
|
|
{
|
|
_bindings.ShowSystemMessage("Please use @help fillcomps for proper usage.");
|
|
return;
|
|
}
|
|
|
|
if (parts.Length > 0 && parts[0].Equals("clear", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_bindings.ClearDesiredComponents();
|
|
_bindings.ShowSystemMessage("Component list cleared.");
|
|
return;
|
|
}
|
|
|
|
uint? category = null;
|
|
uint maximumPrice = 0u;
|
|
foreach (string part in parts)
|
|
{
|
|
if (uint.TryParse(part, out uint price))
|
|
{
|
|
if (price == 0)
|
|
{
|
|
_bindings.ShowSystemMessage("Please specify a value greater than zero.");
|
|
return;
|
|
}
|
|
maximumPrice = price;
|
|
}
|
|
else if (TryGetComponentCategory(part, out uint parsedCategory))
|
|
{
|
|
category = parsedCategory;
|
|
}
|
|
else
|
|
{
|
|
_bindings.ShowSystemMessage("Invalid component type specified.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!_bindings.HasOpenVendor())
|
|
{
|
|
_bindings.ShowSystemMessage("You need an open vendor.");
|
|
return;
|
|
}
|
|
|
|
_bindings.FillComponentBuyList(category, maximumPrice);
|
|
}
|
|
|
|
private static bool TryGetComponentCategory(string value, out uint category)
|
|
{
|
|
category = value.ToLowerInvariant() switch
|
|
{
|
|
"scarab" or "scarabs" => 0u,
|
|
"herb" or "herbs" => 1u,
|
|
"powderedgem" or "powderedgems" or "powder" or "powders" => 2u,
|
|
"alchemicalsubstance" or "alchemicalsubstances" or "potion" or "potions" => 3u,
|
|
"talisman" or "talismans" => 4u,
|
|
"taper" or "tapers" => 5u,
|
|
"pea" or "peas" => 6u,
|
|
_ => uint.MaxValue,
|
|
};
|
|
return category != uint.MaxValue;
|
|
}
|
|
|
|
private static bool TryGetMessageType(string value, out uint type)
|
|
{
|
|
foreach ((uint key, string name) in MessageTypes)
|
|
{
|
|
if (name.Equals(value, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
type = key;
|
|
return true;
|
|
}
|
|
}
|
|
type = 0u;
|
|
return false;
|
|
}
|
|
|
|
private static string FirstArgument(string arguments)
|
|
{
|
|
string trimmed = arguments.Trim();
|
|
int separator = trimmed.IndexOfAny([' ', '\t', '\r', '\n']);
|
|
return separator < 0 ? trimmed : trimmed[..separator];
|
|
}
|
|
|
|
private static string RemainderAfterFirstArgument(string arguments)
|
|
{
|
|
string trimmed = arguments.Trim();
|
|
int separator = trimmed.IndexOfAny([' ', '\t', '\r', '\n']);
|
|
return separator < 0 ? string.Empty : trimmed[(separator + 1)..].TrimStart();
|
|
}
|
|
|
|
private static string[] SplitArguments(string arguments) =>
|
|
arguments.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
private readonly record struct SquelchArguments(
|
|
bool AccountWide, uint MessageType, string Name);
|
|
|
|
private static readonly IReadOnlyDictionary<uint, string> MessageTypes =
|
|
new Dictionary<uint, string>
|
|
{
|
|
[1] = "All",
|
|
[2] = "Speech",
|
|
[3] = "Tell",
|
|
[6] = "Combat",
|
|
[7] = "Magic",
|
|
[12] = "Emote",
|
|
[16] = "Appraisal",
|
|
[17] = "Spellcasting",
|
|
[18] = "Allegiance",
|
|
[19] = "Fellowship",
|
|
[21] = "Combat_Enemy",
|
|
[22] = "Combat_Self",
|
|
[23] = "Recall",
|
|
[24] = "Craft",
|
|
[25] = "Salvaging",
|
|
};
|
|
|
|
private const string StandardEmotes =
|
|
"Standard Emotes:\n"
|
|
+ "Note: These commands should be bound on either side by asterisks. (Example: *wave*)\n"
|
|
+ "ShakeFist; Beckon; BeSeeingYou; BlowKiss; BowDeep; ClapHands; Cry; Laugh; Nod; Point; Shrug; Wave; Akimbo; HeartyLaugh; Salute; TapFoot; WaveHigh; WaveLow; Yawn; Stretch; Cringe; Kneel; Plead; Shiver; Shoo; Slouch; Spit; Surrender; Woah; Winded; YMCA; Eat; Drink; Teapot; Pray; Mock; Cheer; Helper; Warm Hands; Scratch Head; Shake Head\n\n";
|
|
|
|
private const string AwayHelp =
|
|
"@afk - Turns on AFK (away-from-keyboard) mode. When set to AFK, other players that send you directed chatyou will receive a customizable message that your are not currently at the keyboard.\n"
|
|
+ "@afk on - Turns on AFK mode. When set to AFK, other players that send you directed chatyou will receive a customizable message that your are not currently at the keyboard.\n"
|
|
+ "@afk off - Turn off AFK mode.\n"
|
|
+ "@afk msg <message> - Set the message that will be sent to players that send you directed chat while you are in AFK mode. Issuing \"@afk msg\" with no message will set your AFK message back to the default. Your custom AFK message is limited to 192 characters.\n";
|
|
|
|
/// <summary>
|
|
/// Null means the local PublicWeenieDesc has not arrived yet. Retail only
|
|
/// rejects when it has a player object and the required bit is absent, so
|
|
/// unknown follows the same send-and-let-the-server-decide path.
|
|
/// </summary>
|
|
private bool? HasPlayerFlag(EntityCollisionFlags flag)
|
|
{
|
|
uint? bitfield = _bindings.PlayerPublicWeenieBitfield();
|
|
return bitfield is null
|
|
? null
|
|
: (EntityCollisionFlagsExt.FromPwdBitfield(bitfield.Value) & flag) != 0;
|
|
}
|
|
}
|