Should-fix 1: RetailDialogFactory.CloseDialog's queued branch removed the active DialogInfo, ran DialogDone (whose callback can synchronously open a new dialog under the SAME queue key — the two-stage house-abandon confirmation does exactly this), then called OpenNextDialog, which did an unconditional Dictionary.Add on a key the reentrant dialog had already re-occupied. Retail's HashTable::add tolerates the duplicate; Dictionary throws. OpenNextDialog now returns early when the queue key is already active — the reentrant dialog's own eventual close drains the queue. Should-fix 2: @join/@leave wrote the local RuntimeCharacterOptionsState bit before sending, but the Settings Chat toggles reached a second binding (SendSingleCharacterOption) that only sent the wire message, leaving the Turbine membership gate stale until the next PlayerDescription. LiveSessionRuntimeFactory.CreateCommandBindings now has one shared local function for both entrances. Should-fix 3: corrected TS-68/#360 wording again — retail's DoAllegiance dispatcher table EXECUTES boot/ban/officer/title/motd/name/lock/house/ chat/broadcast locally through their own handlers; acdream shows the unrecognized-subcommand refusal for all nine pending the #360 port. What matches retail is the ownership rule (the verb never reaches DoChannelCommand/the server), not the subcommand behavior itself. Removed the inaccurate "matching retail, not merely harmless" / "now matches this" claims from both the register row and the issue. Nits: corrected the HouseAbandonDialogCallback_First citation (0x00580E1A is DoHouse's load site for the callback pointer, not the function entry — the entry is 0x00580240, with the stage-2 confirmation string built at 0x005802D8) in both ClientCommandController.cs and the mirrored test comment; added an InlineData case pinning "@clist allegiance" to RequestChannelList(0x02000000); converted RetailClientCommandCatalog. KnownVerbs from a plain array to a FrozenSet<string> with StringComparer.OrdinalIgnoreCase, matching the file's other lookup tables. Suite: 12,221 passed / 4 skipped / 0 failed (Release), up from CH4's 12,216/4/0 — net +5 tests, no removals. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
527 lines
20 KiB
C#
527 lines
20 KiB
C#
using AcDream.App.UI;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Social;
|
|
using AcDream.UI.Abstractions;
|
|
|
|
namespace AcDream.App.Tests.UI;
|
|
|
|
public sealed class ClientCommandControllerTests
|
|
{
|
|
[Fact]
|
|
public void RecallAndQueryCommands_ExecuteTheirExactBindings()
|
|
{
|
|
var calls = new List<string>();
|
|
var controller = NewController(calls: calls);
|
|
|
|
Execute(ClientCommandId.LifestoneRecall);
|
|
Execute(ClientCommandId.MarketplaceRecall);
|
|
Execute(ClientCommandId.PkArenaRecall);
|
|
Execute(ClientCommandId.PkLiteArenaRecall);
|
|
Execute(ClientCommandId.HouseRecall);
|
|
Execute(ClientCommandId.MansionRecall);
|
|
Execute(ClientCommandId.QueryAge);
|
|
Execute(ClientCommandId.QueryBirth);
|
|
|
|
Assert.Equal(
|
|
["ls", "mp", "pka", "pla", "house", "mansion", "age", "birth"],
|
|
calls);
|
|
|
|
void Execute(ClientCommandId id) => controller.Execute(
|
|
new ExecuteClientCommandCmd(id, Arguments: string.Empty));
|
|
}
|
|
|
|
[Fact]
|
|
public void PkArena_NonPk_ShowsRetailFailureWithoutSending()
|
|
{
|
|
var calls = new List<string>();
|
|
var errors = new List<uint>();
|
|
var controller = NewController(calls, errors, playerBitfield: 0x8u);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.PkArenaRecall, string.Empty));
|
|
|
|
Assert.Empty(calls);
|
|
Assert.Equal([0x055Fu], errors);
|
|
}
|
|
|
|
[Fact]
|
|
public void PkLiteArena_NonPkLite_ShowsRetailFailureWithoutSending()
|
|
{
|
|
var calls = new List<string>();
|
|
var errors = new List<uint>();
|
|
var controller = NewController(calls, errors, playerBitfield: 0x8u);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.PkLiteArenaRecall, string.Empty));
|
|
|
|
Assert.Empty(calls);
|
|
Assert.Equal([0x0560u], errors);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnterPkLite_NonPk_SendsTheRetailGameAction()
|
|
{
|
|
var calls = new List<string>();
|
|
var errors = new List<uint>();
|
|
var controller = NewController(calls, errors, playerBitfield: 0x8u);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.EnterPkLite, string.Empty));
|
|
|
|
Assert.Equal(["pklite"], calls);
|
|
Assert.Empty(errors);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnterPkLite_AlreadyPk_ShowsRetailFailureWithoutSending()
|
|
{
|
|
var calls = new List<string>();
|
|
var errors = new List<uint>();
|
|
// 0x20 = ACCWeenieObject::IsPK bit only.
|
|
var controller = NewController(calls, errors, playerBitfield: 0x20u);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.EnterPkLite, string.Empty));
|
|
|
|
Assert.Empty(calls);
|
|
Assert.Equal([0x0507u], errors);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnterPkLite_AlreadyPkLite_ShowsRetailFailureWithoutSending()
|
|
{
|
|
var calls = new List<string>();
|
|
var errors = new List<uint>();
|
|
// 0x2000000 = ACCWeenieObject::IsPKLite bit only.
|
|
var controller = NewController(calls, errors, playerBitfield: 0x2000000u);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.EnterPkLite, string.Empty));
|
|
|
|
Assert.Empty(calls);
|
|
Assert.Equal([0x0507u], errors);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnterPkLite_UnknownPlayerDescription_SendsRatherThanRejects()
|
|
{
|
|
// Tri-state decision: retail's DoPKLite only rejects when
|
|
// IsPlayerKiller() is TRUE. An indeterminate PublicWeenieDesc (not
|
|
// yet arrived) must send, not reject -- the opposite direction from
|
|
// the arena recalls, which gate on the flag being known-true.
|
|
var calls = new List<string>();
|
|
var errors = new List<uint>();
|
|
var controller = NewController(calls, errors, playerBitfield: null);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.EnterPkLite, string.Empty));
|
|
|
|
Assert.Equal(["pklite"], calls);
|
|
Assert.Empty(errors);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingPlayerDescription_DoesNotInventAClientRejection()
|
|
{
|
|
var calls = new List<string>();
|
|
var controller = NewController(calls, playerBitfield: null);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.PkArenaRecall, string.Empty));
|
|
|
|
Assert.Equal(["pka"], calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void LocalPresentationCommands_UseApplicationServices()
|
|
{
|
|
var calls = new List<string>();
|
|
var messages = new List<string>();
|
|
var controller = NewController(calls, messages: messages);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.ToggleFrameRate, ""));
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.ToggleUiLock, ""));
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.ShowVersion, ""));
|
|
|
|
Assert.Equal(["fps", "lock"], calls);
|
|
Assert.Equal(["Client version 1.2.3"], messages);
|
|
}
|
|
|
|
[Fact]
|
|
public void Die_RequiresRetailConfirmationBeforeSuicide()
|
|
{
|
|
var calls = new List<string>();
|
|
var controller = NewController(calls);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.Die, ""));
|
|
|
|
Assert.Equal(
|
|
[
|
|
"confirm:Do you really want to kill your character? You may drop items and accrue a vitae penalty.",
|
|
"suicide",
|
|
],
|
|
calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChatAndLayoutCommands_UseNamedAndAutomaticPersistenceBindings()
|
|
{
|
|
var calls = new List<string>();
|
|
var messages = new List<string>();
|
|
var controller = NewController(calls, messages: messages);
|
|
|
|
Execute(ClientCommandId.ClearChat, "all");
|
|
Execute(ClientCommandId.SaveUi, "hunt");
|
|
Execute(ClientCommandId.LoadUi, "hunt");
|
|
Execute(ClientCommandId.SaveAutoUi, string.Empty);
|
|
Execute(ClientCommandId.LoadAutoUi, string.Empty);
|
|
Execute(ClientCommandId.SaveUi, "this-name-is-over-16-characters");
|
|
|
|
Assert.Equal(
|
|
["clear:True", "saveui:hunt", "loadui:hunt", "saveautoui", "loadautoui"],
|
|
calls);
|
|
Assert.Equal(["The file name must be 16 characters or less."], messages);
|
|
|
|
void Execute(ClientCommandId id, string arguments) =>
|
|
controller.Execute(new ExecuteClientCommandCmd(id, arguments));
|
|
}
|
|
|
|
[Fact]
|
|
public void AwayCommands_RespectCurrentModeAndPackRetailMessageForm()
|
|
{
|
|
var calls = new List<string>();
|
|
var messages = new List<string>();
|
|
var controller = NewController(calls, messages: messages, isAway: false);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.Away, "on"));
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.Away, "msg Stepped away"));
|
|
|
|
Assert.Equal(["afk:True", "afkmsg:Stepped away\n"], calls);
|
|
Assert.Equal(["New AFK message set: Stepped away\n"], messages);
|
|
|
|
calls.Clear();
|
|
controller = NewController(calls, isAway: true);
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.Away, "off"));
|
|
Assert.Equal(["afk:False"], calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConsentAndEmoteCommands_RouteTheirExactActions()
|
|
{
|
|
var calls = new List<string>();
|
|
var controller = NewController(calls, acceptsLootPermits: false);
|
|
|
|
Execute(ClientCommandId.Consent, "on");
|
|
Execute(ClientCommandId.Consent, "who");
|
|
Execute(ClientCommandId.Consent, "clear");
|
|
Execute(ClientCommandId.Consent, "remove Alice Example");
|
|
Execute(ClientCommandId.Emote, "waves happily");
|
|
|
|
Assert.Equal(
|
|
[
|
|
"consentmode:True",
|
|
"consentwho",
|
|
"consentclear",
|
|
"consentremove:Alice Example",
|
|
"emote:waves happily",
|
|
],
|
|
calls);
|
|
|
|
void Execute(ClientCommandId id, string arguments) =>
|
|
controller.Execute(new ExecuteClientCommandCmd(id, arguments));
|
|
}
|
|
|
|
[Fact]
|
|
public void FriendsCommands_ReadAuthoritativeStateAndSendObjectId()
|
|
{
|
|
var friends = new FriendsState();
|
|
friends.Apply(new FriendsUpdate(
|
|
FriendsUpdateType.Full,
|
|
[
|
|
new FriendEntry(0x50000001u, "Alice", true, false, [], []),
|
|
new FriendEntry(0x50000002u, "Bjørn", false, false, [], []),
|
|
]));
|
|
var calls = new List<string>();
|
|
var messages = new List<string>();
|
|
var controller = NewController(calls, messages: messages, friends: friends);
|
|
|
|
Execute(ClientCommandId.Friends, "online");
|
|
Execute(ClientCommandId.FriendsRemove, "alice");
|
|
Execute(ClientCommandId.FriendsAdd, "Cara");
|
|
Execute(ClientCommandId.Friends, "old");
|
|
|
|
Assert.Contains("Alice (Online)", messages.Single());
|
|
Assert.DoesNotContain("Bjørn", messages.Single());
|
|
Assert.Equal(
|
|
["friendremove:1342177281", "friendadd:Cara", "friendsold"],
|
|
calls);
|
|
|
|
void Execute(ClientCommandId id, string arguments) =>
|
|
controller.Execute(new ExecuteClientCommandCmd(id, arguments));
|
|
}
|
|
|
|
[Fact]
|
|
public void SquelchAndFilterCommands_ParseRetailOptions()
|
|
{
|
|
var calls = new List<string>();
|
|
var controller = NewController(calls, lastTeller: "Recent Teller");
|
|
|
|
Execute(ClientCommandId.Squelch, "-account Alice");
|
|
Execute(ClientCommandId.Unsquelch, "-reply");
|
|
Execute(ClientCommandId.Squelch, "-Magic Caster Name");
|
|
Execute(ClientCommandId.Filter, "-Combat_Self");
|
|
Execute(ClientCommandId.Unfilter, "-Tell");
|
|
|
|
Assert.Equal(
|
|
[
|
|
"accountsquelch:True:Alice",
|
|
"charsquelch:False:0:Recent Teller:1",
|
|
"charsquelch:True:0:Caster Name:7",
|
|
"globalsquelch:True:22",
|
|
"globalsquelch:False:3",
|
|
],
|
|
calls);
|
|
|
|
void Execute(ClientCommandId id, string arguments) =>
|
|
controller.Execute(new ExecuteClientCommandCmd(id, arguments));
|
|
}
|
|
|
|
[Fact]
|
|
public void FillComponents_ClearWorksWithoutVendorAndBuyingRequiresOne()
|
|
{
|
|
var calls = new List<string>();
|
|
var messages = new List<string>();
|
|
var controller = NewController(calls, messages: messages, vendorOpen: false);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.FillComponents, "clear"));
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.FillComponents, "scarabs 500"));
|
|
|
|
Assert.Equal(["clearcomps"], calls);
|
|
Assert.Equal(["Component list cleared.", "You need an open vendor."], messages);
|
|
}
|
|
|
|
// ── CH4 REJECT-review Blocker 2 (2026-08-09) ────────────────────────
|
|
// "@house abandon" must run retail's real two-stage confirmation
|
|
// (DoHouse's abandon branch @0x00580D58 → HouseAbandonDialogCallback_
|
|
// First, function entry @0x00580240 (0x00580E1A is only DoHouse's load
|
|
// site for the callback pointer), whose own stage-2 string site
|
|
// @0x005802D8 opens HouseAbandonDialogCallback_Second @0x0057BE90,
|
|
// the ONLY Event_AbandonHouse() call site @0x0057BF01) before sending
|
|
// 0x021F — previously it sent immediately with no confirmation at all.
|
|
// Citations corrected 2026-08-09, CH4 re-review nit 4.
|
|
|
|
[Fact]
|
|
public void HouseAbandon_BothStagesAccepted_ShowsBothPromptsThenSendsExactlyOnce()
|
|
{
|
|
var calls = new List<string>();
|
|
var controller = NewController(calls);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.HouseAbandon, ""));
|
|
|
|
Assert.Equal(
|
|
[
|
|
"confirm:Do you really want to abandon your house? Any items in the house (on hooks or in storage) will stay with the house, and you will lose access to them.",
|
|
"confirm:Are you absolutely certain you wish to abandon your house? Click yes only if you are sure!",
|
|
"houseabandon",
|
|
],
|
|
calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void HouseAbandon_DeclineFirstStage_ShowsOnlyOnePromptAndNeverSends()
|
|
{
|
|
var calls = new List<string>();
|
|
var controller = NewController(calls, confirmationResponses: new Queue<bool>([false]));
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.HouseAbandon, ""));
|
|
|
|
Assert.Equal(
|
|
[
|
|
"confirm:Do you really want to abandon your house? Any items in the house (on hooks or in storage) will stay with the house, and you will lose access to them.",
|
|
],
|
|
calls);
|
|
Assert.DoesNotContain("houseabandon", calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void HouseAbandon_DeclineSecondStage_ShowsBothPromptsAndNeverSends()
|
|
{
|
|
var calls = new List<string>();
|
|
var controller = NewController(calls, confirmationResponses: new Queue<bool>([true, false]));
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.HouseAbandon, ""));
|
|
|
|
Assert.Equal(
|
|
[
|
|
"confirm:Do you really want to abandon your house? Any items in the house (on hooks or in storage) will stay with the house, and you will lose access to them.",
|
|
"confirm:Are you absolutely certain you wish to abandon your house? Click yes only if you are sure!",
|
|
],
|
|
calls);
|
|
Assert.DoesNotContain("houseabandon", calls);
|
|
}
|
|
|
|
// ── CH4 REJECT-review SHOULD-FIX 5 (2026-08-09) ─────────────────────
|
|
// "@permit add/remove <multi-word name>" joins every token after the
|
|
// mode word into the name (retail's JoinArgsAsName).
|
|
|
|
[Fact]
|
|
public void Permit_MultiWordName_JoinsTheRemainderIntoOneName()
|
|
{
|
|
var calls = new List<string>();
|
|
var controller = NewController(calls);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.Permit, "add Aunt Agatha"));
|
|
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.Permit, "remove Lord Gnarly Beard"));
|
|
|
|
Assert.Equal(
|
|
["permitadd:Aunt Agatha", "permitremove:Lord Gnarly Beard"],
|
|
calls);
|
|
}
|
|
|
|
// ── CH4 REJECT-review SHOULD-FIX 6 (2026-08-09) ─────────────────────
|
|
// @clist/@on/@off with an unresolvable (but single-token) tag raises
|
|
// retail's WeenieError 0x422 ("That channel doesn't exist.") instead
|
|
// of silently doing nothing.
|
|
|
|
[Fact]
|
|
public void ChannelArgumentCommands_UnknownTag_ShowsWeenieError422WithoutSending()
|
|
{
|
|
var calls = new List<string>();
|
|
var errors = new List<uint>();
|
|
var controller = NewController(calls, errors);
|
|
|
|
Execute(ClientCommandId.ListChannel, "nonsense");
|
|
Execute(ClientCommandId.OnChannel, "nonsense");
|
|
Execute(ClientCommandId.OffChannel, "nonsense");
|
|
|
|
Assert.Empty(calls);
|
|
Assert.Equal([0x0422u, 0x0422u, 0x0422u], errors);
|
|
|
|
void Execute(ClientCommandId id, string arguments) =>
|
|
controller.Execute(new ExecuteClientCommandCmd(id, arguments));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(ClientCommandId.ListChannel, "fellowship", "clist:2048")]
|
|
[InlineData(ClientCommandId.OnChannel, "admin", "on:2")]
|
|
[InlineData(ClientCommandId.OffChannel, "sentinel", "off:512")]
|
|
// CH4 re-review nit 5 (2026-08-09): pins "@clist allegiance" to
|
|
// RequestChannelList(0x02000000) — retail's real Allegiance channel id
|
|
// (RetailChannelTagTable's "allegiance"/"a"/"ab" entries), distinct
|
|
// from the small legacy-channel ids the rows above cover.
|
|
[InlineData(ClientCommandId.ListChannel, "allegiance", "clist:33554432")]
|
|
public void ChannelArgumentCommands_KnownTag_SendsWithoutError(
|
|
ClientCommandId id, string tag, string expectedCall)
|
|
{
|
|
var calls = new List<string>();
|
|
var errors = new List<uint>();
|
|
var controller = NewController(calls, errors);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(id, tag));
|
|
|
|
Assert.Empty(errors);
|
|
Assert.Equal([expectedCall], calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnknownCommandId_FailsAtApplicationBoundary()
|
|
{
|
|
var controller = NewController();
|
|
var command = new ExecuteClientCommandCmd((ClientCommandId)999, string.Empty);
|
|
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => controller.Execute(command));
|
|
}
|
|
|
|
private static ClientCommandController NewController(
|
|
List<string>? calls = null,
|
|
List<uint>? errors = null,
|
|
uint? playerBitfield = 0x02000028u,
|
|
List<string>? messages = null,
|
|
bool isAway = false,
|
|
bool acceptsLootPermits = true,
|
|
FriendsState? friends = null,
|
|
SquelchState? squelch = null,
|
|
string? lastTeller = null,
|
|
bool vendorOpen = false,
|
|
// CH4 REJECT-review Blocker 2 (2026-08-09): lets a test drive a
|
|
// specific accept/decline sequence through consecutive
|
|
// ShowConfirmation calls (e.g. house-abandon's two-stage prompt).
|
|
// Defaults to "always accept" so every pre-existing single-stage
|
|
// test (Die, etc.) keeps its original behavior unchanged.
|
|
Queue<bool>? confirmationResponses = null)
|
|
{
|
|
calls ??= [];
|
|
errors ??= [];
|
|
messages ??= [];
|
|
return new ClientCommandController(new ClientCommandController.Bindings(
|
|
() => calls.Add("ls"),
|
|
() => calls.Add("mp"),
|
|
() => calls.Add("pka"),
|
|
() => calls.Add("pla"),
|
|
() => calls.Add("house"),
|
|
() => calls.Add("mansion"),
|
|
() => calls.Add("age"),
|
|
() => calls.Add("birth"),
|
|
() => calls.Add("fps"),
|
|
() => calls.Add("lock"),
|
|
messages.Add,
|
|
errors.Add,
|
|
() => playerBitfield,
|
|
() => "1.2.3",
|
|
() => new Position(
|
|
0xA9B40001u,
|
|
new CellFrame(new System.Numerics.Vector3(1f, 2f, 3f),
|
|
System.Numerics.Quaternion.Identity)),
|
|
() => null,
|
|
(message, completed) =>
|
|
{
|
|
calls.Add($"confirm:{message}");
|
|
bool accepted = confirmationResponses is { Count: > 0 }
|
|
? confirmationResponses.Dequeue()
|
|
: true;
|
|
completed(accepted);
|
|
},
|
|
() => calls.Add("suicide"),
|
|
all => calls.Add("clear:" + all),
|
|
name => calls.Add("saveui:" + name),
|
|
name => calls.Add("loadui:" + name),
|
|
() => calls.Add("saveautoui"),
|
|
() => calls.Add("loadautoui"),
|
|
() => isAway,
|
|
away => calls.Add("afk:" + away),
|
|
message => calls.Add("afkmsg:" + message),
|
|
() => acceptsLootPermits,
|
|
enabled => calls.Add("consentmode:" + enabled),
|
|
() => calls.Add("consentwho"),
|
|
() => calls.Add("consentclear"),
|
|
name => calls.Add("consentremove:" + name),
|
|
message => calls.Add("emote:" + message),
|
|
friends ?? new FriendsState(),
|
|
name => calls.Add("friendadd:" + name),
|
|
id => calls.Add("friendremove:" + id),
|
|
() => calls.Add("friendsclear"),
|
|
() => calls.Add("friendsold"),
|
|
squelch ?? new SquelchState(),
|
|
(add, id, name, type) => calls.Add($"charsquelch:{add}:{id}:{name}:{type}"),
|
|
(add, name) => calls.Add($"accountsquelch:{add}:{name}"),
|
|
(add, type) => calls.Add($"globalsquelch:{add}:{type}"),
|
|
() => lastTeller,
|
|
() => calls.Add("clearcomps"),
|
|
() => vendorOpen,
|
|
(category, price) => calls.Add($"fillcomps:{category}:{price}"),
|
|
() => calls.Add("pklite"),
|
|
() => false,
|
|
title => calls.Add("title:" + title),
|
|
(optionId, value) => calls.Add($"charoption:{optionId}:{value}"),
|
|
name => calls.Add("permitadd:" + name),
|
|
name => calls.Add("permitremove:" + name),
|
|
houseType => calls.Add("hslist:" + houseType),
|
|
() => calls.Add("index"),
|
|
channelId => calls.Add("clist:" + channelId),
|
|
channelId => calls.Add("on:" + channelId),
|
|
channelId => calls.Add("off:" + channelId),
|
|
() => calls.Add("alh"),
|
|
name => calls.Add("alleginfo:" + name),
|
|
() => calls.Add("houseabandon")));
|
|
}
|
|
}
|