1064 lines
44 KiB
C#
1064 lines
44 KiB
C#
using System.Globalization;
|
|
using AcDream.App.UI;
|
|
using AcDream.Core.Chat;
|
|
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));
|
|
}
|
|
|
|
// ── CT-B4: retail's @log ────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Log_WithAName_ReportsWhereChatIsGoingAndHowToStop()
|
|
{
|
|
// Retail's reply, byte-decoded from the paired binary — Binary Ninja
|
|
// truncates it at "Copying chat to %s. Run command…". Note the TWO
|
|
// spaces after the period; they are retail's.
|
|
var messages = new List<string>();
|
|
var calls = new List<string>();
|
|
ClientCommandController ctrl = NewController(
|
|
calls, messages: messages,
|
|
chatLog: name => new ChatLogResult(true, false, name, null));
|
|
|
|
ctrl.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.ChatLogFile, "aclog.txt"));
|
|
|
|
Assert.Contains("log:aclog.txt", calls);
|
|
Assert.Equal(
|
|
"Copying chat to aclog.txt. Run command again with no arguments "
|
|
+ "to turn off logging.",
|
|
Assert.Single(messages));
|
|
}
|
|
|
|
[Fact]
|
|
public void Log_WhenTheFileCannotBeOpened_SaysSoRatherThanClaimingSuccess()
|
|
{
|
|
var messages = new List<string>();
|
|
ClientCommandController ctrl = NewController(
|
|
messages: messages,
|
|
chatLog: name => new ChatLogResult(false, false, name, null));
|
|
|
|
ctrl.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.ChatLogFile, "C:/nope/x.txt"));
|
|
|
|
Assert.Equal(
|
|
"Failed to redirect to file C:/nope/x.txt!",
|
|
Assert.Single(messages));
|
|
}
|
|
|
|
[Fact]
|
|
public void Log_WithNoArgument_ClosesTheOpenLogAndSaysBothLines()
|
|
{
|
|
// CloseLogFile announces the file, then DoSetOutput announces the
|
|
// redirect. Two lines, in that order.
|
|
var messages = new List<string>();
|
|
ClientCommandController ctrl = NewController(
|
|
messages: messages,
|
|
chatLog: _ => new ChatLogResult(false, true, string.Empty, "aclog.txt"));
|
|
|
|
ctrl.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.ChatLogFile, ""));
|
|
|
|
Assert.Equal(
|
|
["Chat log aclog.txt closed.", "Chat output now directed only to the screen."],
|
|
messages);
|
|
}
|
|
|
|
[Fact]
|
|
public void Log_WithNoArgumentAndNothingOpen_AsksForAFileName()
|
|
{
|
|
// The same verb with the same arguments says something DIFFERENT
|
|
// depending on whether a log was running — retail branches on
|
|
// CloseLogFile's return value, not on the arguments.
|
|
var messages = new List<string>();
|
|
ClientCommandController ctrl = NewController(
|
|
messages: messages,
|
|
chatLog: _ => new ChatLogResult(false, false, string.Empty, null));
|
|
|
|
ctrl.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.ChatLogFile, ""));
|
|
|
|
Assert.Equal(
|
|
"Please specify a file to append chat messages to.",
|
|
Assert.Single(messages));
|
|
}
|
|
|
|
[Fact]
|
|
public void Log_StartingASecondLogAnnouncesThatTheFirstEnded()
|
|
{
|
|
var messages = new List<string>();
|
|
ClientCommandController ctrl = NewController(
|
|
messages: messages,
|
|
chatLog: name => new ChatLogResult(true, true, name, "old.txt"));
|
|
|
|
ctrl.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.ChatLogFile, "new.txt"));
|
|
|
|
Assert.Equal("Chat log old.txt closed.", messages[0]);
|
|
Assert.StartsWith("Copying chat to new.txt.", messages[1]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Log_TakesTheWholeRemainderSoASpacedNameSurvives()
|
|
{
|
|
// Retail JoinArgs the arguments before using them as a filename.
|
|
var calls = new List<string>();
|
|
ClientCommandController ctrl = NewController(
|
|
calls,
|
|
chatLog: name => new ChatLogResult(true, false, name, null));
|
|
|
|
ctrl.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.ChatLogFile, "my chat log.txt"));
|
|
|
|
Assert.Contains("log:my chat log.txt", calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void Log_ResolvesFromTheCatalogWithItsWholeRemainderAsTheArgument()
|
|
{
|
|
// The verb had a help entry since CH4 but no catalog entry, so /log
|
|
// printed help and did nothing. This pins the registration.
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(
|
|
"/log my chat log.txt", out RetailClientCommandCatalog.Match match));
|
|
|
|
Assert.Equal(ClientCommandId.ChatLogFile, match.Command);
|
|
Assert.Equal("my chat log.txt", match.Arguments);
|
|
}
|
|
|
|
// ── #361: retail's @day / @render ─────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Day_TogglesThePersistentOptionAndPrintsRetailsExactLines()
|
|
{
|
|
bool persistentDaylight = false;
|
|
var values = new List<bool>();
|
|
var messages = new List<string>();
|
|
ClientCommandController controller = NewController(
|
|
messages: messages,
|
|
isPersistentDaylight: () => persistentDaylight,
|
|
setPersistentDaylight: value =>
|
|
{
|
|
persistentDaylight = value;
|
|
values.Add(value);
|
|
});
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.TogglePersistentDaylight,
|
|
"ignored exactly like retail"));
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.TogglePersistentDaylight,
|
|
string.Empty));
|
|
|
|
Assert.Equal([true, false], values);
|
|
Assert.Equal(
|
|
["Let there be light!", "Normality has been restored."],
|
|
messages);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("radius 5", 5)]
|
|
[InlineData("RADIUS 25 extra ignored", 25)]
|
|
[InlineData("radius 12suffix", 12)]
|
|
public void RenderRadius_AcceptsRetailRangeAndAtoiPrefix(
|
|
string arguments,
|
|
int expected)
|
|
{
|
|
var radii = new List<int>();
|
|
var messages = new List<string>();
|
|
ClientCommandController controller = NewController(
|
|
messages: messages,
|
|
setLandscapeRadius: radii.Add);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.RenderOption,
|
|
arguments));
|
|
|
|
Assert.Equal([expected], radii);
|
|
Assert.Equal(["Landscape radius set"], messages);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("fov 10", 10f)]
|
|
[InlineData("FOV 160 extra", 160f)]
|
|
[InlineData("fov 91degrees", 91f)]
|
|
public void RenderFov_AcceptsRetailRangeAndAtoiPrefix(
|
|
string arguments,
|
|
float expected)
|
|
{
|
|
var values = new List<float>();
|
|
var messages = new List<string>();
|
|
ClientCommandController controller = NewController(
|
|
messages: messages,
|
|
setFieldOfView: values.Add);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.RenderOption,
|
|
arguments));
|
|
|
|
Assert.Equal([expected], values);
|
|
Assert.Equal(["Field of view set"], messages);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("radius", "Must specify a radius")]
|
|
[InlineData("radius 4", "Radius must be between 5 and 25")]
|
|
[InlineData("radius nope", "Radius must be between 5 and 25")]
|
|
[InlineData("fov", "Must specify a field of view")]
|
|
[InlineData("fov 161", "Field of view must be between 10 and 160")]
|
|
public void Render_InvalidValuesPrintRetailsExactReply(
|
|
string arguments,
|
|
string expected)
|
|
{
|
|
var calls = new List<string>();
|
|
var messages = new List<string>();
|
|
ClientCommandController controller = NewController(
|
|
calls,
|
|
messages: messages);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.RenderOption,
|
|
arguments));
|
|
|
|
Assert.DoesNotContain(calls, call =>
|
|
call.StartsWith("radius:", StringComparison.Ordinal)
|
|
|| call.StartsWith("fov:", StringComparison.Ordinal));
|
|
Assert.Equal([expected], messages);
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_UsageAndUnknownOptionMatchRetail()
|
|
{
|
|
var messages = new List<string>();
|
|
ClientCommandController controller = NewController(messages: messages);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.RenderOption,
|
|
string.Empty));
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.RenderOption,
|
|
"usage"));
|
|
controller.Execute(new ExecuteClientCommandCmd(
|
|
ClientCommandId.RenderOption,
|
|
"unknown 1"));
|
|
|
|
string usage = RetailCommandHelpTable.Render.TrimEnd('\n');
|
|
Assert.Equal([usage, usage], messages);
|
|
}
|
|
|
|
[Fact]
|
|
public void AllegianceAdministration_ExecutesEveryRetailDispatcherBranch()
|
|
{
|
|
var calls = new List<string>();
|
|
var system = new List<string>();
|
|
var clientLocal = new List<string>();
|
|
ClientCommandController controller = NewController(
|
|
calls: calls,
|
|
messages: system,
|
|
clientLocalMessages: clientLocal);
|
|
|
|
Execute(ClientCommandId.AllegianceInfo, "Lord Bob");
|
|
Execute(ClientCommandId.AllegianceBoot, "Lord Bob");
|
|
Execute(ClientCommandId.AllegianceBoot, "-account Account Bob");
|
|
// Retail validates non-empty BEFORE removing -account, so this odd
|
|
// form deliberately sends an empty name with accountBoot=true.
|
|
Execute(ClientCommandId.AllegianceBoot, "-account");
|
|
Execute(ClientCommandId.AllegianceBan, "list ignored");
|
|
Execute(ClientCommandId.AllegianceBan, "add Lord Bob");
|
|
Execute(ClientCommandId.AllegianceBan, "remove Lord Bob");
|
|
Execute(ClientCommandId.AllegianceChat, "on");
|
|
Execute(ClientCommandId.AllegianceChat, "off");
|
|
Execute(ClientCommandId.AllegianceChat, "kick Bob");
|
|
Execute(ClientCommandId.AllegianceChat, "kick Bob, Bad manners");
|
|
Execute(ClientCommandId.AllegianceChat, "kick ");
|
|
Execute(ClientCommandId.AllegianceChat, "gag Lord Bob");
|
|
Execute(ClientCommandId.AllegianceChat, "ungag Lord Bob");
|
|
Execute(ClientCommandId.AllegianceBroadcast, "Hear ye");
|
|
Execute(ClientCommandId.AllegianceOfficer, "");
|
|
Execute(ClientCommandId.AllegianceOfficer, "clear ignored");
|
|
Execute(ClientCommandId.AllegianceOfficer, "remove Lord Bob");
|
|
Execute(ClientCommandId.AllegianceOfficer, "add 0x2 Lord Bob");
|
|
Execute(ClientCommandId.AllegianceOfficer, "set 03 Aunt Alice");
|
|
Execute(ClientCommandId.AllegianceOfficerTitle, "");
|
|
Execute(ClientCommandId.AllegianceOfficerTitle, "clear ignored");
|
|
Execute(ClientCommandId.AllegianceOfficerTitle, "set 0x2 High Regent");
|
|
Execute(ClientCommandId.AllegianceOfficerTitle, "set 1");
|
|
Execute(ClientCommandId.AllegianceName, "");
|
|
Execute(ClientCommandId.AllegianceName, "set The Best Allegiance");
|
|
Execute(ClientCommandId.AllegianceName, "set");
|
|
Execute(ClientCommandId.AllegianceName, "clear ignored");
|
|
Execute(ClientCommandId.AllegianceLock, "");
|
|
Execute(ClientCommandId.AllegianceLock, "off");
|
|
Execute(ClientCommandId.AllegianceLock, "on");
|
|
Execute(ClientCommandId.AllegianceLock, "toggle");
|
|
Execute(ClientCommandId.AllegianceLock, "check");
|
|
Execute(ClientCommandId.AllegianceLock, "bypass");
|
|
Execute(ClientCommandId.AllegianceLock, "bypass clear");
|
|
Execute(ClientCommandId.AllegianceLock, "bypass Lord Bob");
|
|
Execute(ClientCommandId.AllegianceHouse, "");
|
|
Execute(ClientCommandId.AllegianceHouse, "guest open");
|
|
Execute(ClientCommandId.AllegianceHouse, "guest close");
|
|
Execute(ClientCommandId.AllegianceHouse, "storage open");
|
|
Execute(ClientCommandId.AllegianceHouse, "storage close");
|
|
Execute(ClientCommandId.AllegianceMotd, "");
|
|
Execute(ClientCommandId.AllegianceMotd, "set Welcome everyone");
|
|
Execute(ClientCommandId.AllegianceMotd, "set");
|
|
Execute(ClientCommandId.AllegianceMotd, "clear ignored");
|
|
|
|
Assert.Equal(
|
|
[
|
|
"alleginfo:Lord Bob",
|
|
"allegboot:Lord Bob:False",
|
|
"allegboot:Account Bob:True",
|
|
"allegboot::True",
|
|
"allegban:list",
|
|
"allegban:add:Lord Bob",
|
|
"allegban:remove:Lord Bob",
|
|
"charoption:27:True",
|
|
"charoption:27:False",
|
|
"allegchatboot:Bob:No reason given.",
|
|
"allegchatboot:Bob:Bad manners",
|
|
"allegchatboot::No reason given.",
|
|
"allegchatgag:Lord Bob:True",
|
|
"allegchatgag:Lord Bob:False",
|
|
"allegbroadcast:Hear ye",
|
|
"allegofficer:list",
|
|
"allegofficer:clear",
|
|
"allegofficer:remove:Lord Bob",
|
|
"allegofficer:set:2:Lord Bob",
|
|
"allegofficer:set:3:Aunt Alice",
|
|
"allegtitle:list",
|
|
"allegtitle:clear",
|
|
"allegtitle:set:2:High Regent",
|
|
"allegtitle:set:1:",
|
|
"allegname:query",
|
|
"allegname:set:The Best Allegiance",
|
|
"allegname:set:",
|
|
"allegname:clear",
|
|
"alleglock:4",
|
|
"alleglock:1",
|
|
"alleglock:2",
|
|
"alleglock:3",
|
|
"alleglock:4",
|
|
"alleglock:5",
|
|
"alleglock:6",
|
|
"alleglock:bypass:Lord Bob",
|
|
"alleghouse:1",
|
|
"alleghouse:2",
|
|
"alleghouse:3",
|
|
"alleghouse:4",
|
|
"alleghouse:5",
|
|
"motd:query",
|
|
"motd:set:Welcome everyone",
|
|
"motd:set:",
|
|
"motd:clear",
|
|
],
|
|
calls);
|
|
Assert.Equal(
|
|
[
|
|
"Attempting to boot Lord Bob...",
|
|
"Attempting to boot Account Bob (Account)...",
|
|
"Attempting to boot (Account)...",
|
|
],
|
|
system);
|
|
Assert.Empty(clientLocal);
|
|
|
|
void Execute(ClientCommandId command, string arguments) =>
|
|
controller.Execute(new ExecuteClientCommandCmd(command, arguments));
|
|
}
|
|
|
|
[Fact]
|
|
public void HouseAdministration_ExecutesEveryRetailDispatcherBranch()
|
|
{
|
|
var calls = new List<string>();
|
|
ClientCommandController controller = NewController(calls: calls);
|
|
|
|
Execute(ClientCommandId.HouseOpenStatus, "open");
|
|
Execute(ClientCommandId.HouseOpenStatus, "close");
|
|
Execute(ClientCommandId.HouseGuests, "add Lord Bob");
|
|
Execute(ClientCommandId.HouseGuests, "remove Lord Bob");
|
|
Execute(ClientCommandId.HouseGuests, "remove_all ignored");
|
|
Execute(ClientCommandId.HouseGuests, "list ignored");
|
|
Execute(ClientCommandId.HouseGuests, "show ignored");
|
|
Execute(ClientCommandId.HouseGuests, "add_allegiance ignored");
|
|
Execute(ClientCommandId.HouseGuests, "remove_allegiance ignored");
|
|
Execute(ClientCommandId.HouseStorage, "add Lord Bob");
|
|
Execute(ClientCommandId.HouseStorage, "remove Lord Bob");
|
|
Execute(ClientCommandId.HouseStorage, "add -all");
|
|
Execute(ClientCommandId.HouseStorage, "remove -all");
|
|
Execute(ClientCommandId.HouseStorage, "remove_all ignored");
|
|
Execute(ClientCommandId.HouseStorage, "list ignored");
|
|
Execute(ClientCommandId.HouseStorage, "show ignored");
|
|
Execute(ClientCommandId.HouseStorage, "add_allegiance ignored");
|
|
Execute(ClientCommandId.HouseStorage, "remove_allegiance ignored");
|
|
Execute(ClientCommandId.HouseBoot, "Lord Bob");
|
|
Execute(ClientCommandId.HouseBoot, "-all");
|
|
Execute(ClientCommandId.HouseBootAll, "ignored");
|
|
Execute(ClientCommandId.HouseHooks, "on ignored");
|
|
Execute(ClientCommandId.HouseHooks, "off ignored");
|
|
|
|
Assert.Equal(
|
|
[
|
|
"houseopen:True",
|
|
"houseopen:False",
|
|
"houseguest:add:Lord Bob",
|
|
"houseguest:remove:Lord Bob",
|
|
"houseguest:remove_all",
|
|
"houseguest:list",
|
|
"houseguest:list",
|
|
"houseguest:allegiance:True",
|
|
"houseguest:allegiance:False",
|
|
"housestorage:True:Lord Bob",
|
|
"housestorage:False:Lord Bob",
|
|
"housestorage:add_all",
|
|
"housestorage:remove_all",
|
|
"housestorage:remove_all",
|
|
"houseguest:list",
|
|
"houseguest:list",
|
|
"housestorage:allegiance:True",
|
|
"housestorage:allegiance:False",
|
|
"houseboot:Lord Bob",
|
|
"houseboot:all",
|
|
"houseboot:all",
|
|
"househooks:True",
|
|
"househooks:False",
|
|
],
|
|
calls);
|
|
|
|
void Execute(ClientCommandId command, string arguments) =>
|
|
controller.Execute(new ExecuteClientCommandCmd(command, arguments));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(ClientCommandId.AllegianceInfo, "", "Please specify an actual name.")]
|
|
[InlineData(ClientCommandId.AllegianceBoot, "", "Please specify an actual name.")]
|
|
[InlineData(ClientCommandId.AllegianceBan, "add", "Please specify an actual name.")]
|
|
[InlineData(ClientCommandId.AllegianceChat, "gag", "Please specify an actual name.")]
|
|
[InlineData(ClientCommandId.AllegianceBroadcast, "", "Please see @help Allegiance for more information on how to use this command.")]
|
|
[InlineData(ClientCommandId.AllegianceOfficer, "remove", "Please specify the name of an allegiance member.")]
|
|
[InlineData(ClientCommandId.AllegianceOfficer, "add nope Bob", "Please specify a valid officer level as a number between 1 and 3. Check the game help files for more information on officer levels.")]
|
|
[InlineData(ClientCommandId.AllegianceOfficer, "add 2", "Please specify the name of an allegiance member.")]
|
|
[InlineData(ClientCommandId.AllegianceOfficerTitle, "set 4 Regent", "Please specify a valid officer level as a number between 1 and 3.")]
|
|
[InlineData(ClientCommandId.AllegianceName, "nope", "Please see @help Allegiance for more information on how to use this command.")]
|
|
[InlineData(ClientCommandId.AllegianceLock, "nope", "Please see @help Allegiance for more information on how to use this command.")]
|
|
[InlineData(ClientCommandId.AllegianceHouse, "guest nope", "Please see @help Allegiance for more information on how to use this command.")]
|
|
[InlineData(ClientCommandId.AllegianceMotd, "nope", "Please see @help Allegiance for more information on how to use this command.")]
|
|
[InlineData(ClientCommandId.AllegianceUnrecognizedSubcommand, "nope", "Please see @help Allegiance for more information on how to use this command.")]
|
|
[InlineData(ClientCommandId.HouseGuests, "add", "Please specify the guest's name.")]
|
|
[InlineData(ClientCommandId.HouseStorage, "add", "Please specify an actual name.")]
|
|
[InlineData(ClientCommandId.HouseBoot, "", "Please see @help House for more information on how to use this command.")]
|
|
[InlineData(ClientCommandId.HouseHooks, "maybe", "Please see @help House for more information on how to use this command.")]
|
|
[InlineData(ClientCommandId.HouseUnrecognizedSubcommand, "nope", "Please see @help House for more information on how to use this command.")]
|
|
public void AdministrationInvalidForms_EmitExactRetailClientLocalText(
|
|
ClientCommandId command,
|
|
string arguments,
|
|
string expected)
|
|
{
|
|
var calls = new List<string>();
|
|
var system = new List<string>();
|
|
var clientLocal = new List<string>();
|
|
ClientCommandController controller = NewController(
|
|
calls: calls,
|
|
messages: system,
|
|
clientLocalMessages: clientLocal);
|
|
|
|
controller.Execute(new ExecuteClientCommandCmd(command, arguments));
|
|
|
|
Assert.Empty(calls);
|
|
Assert.Empty(system);
|
|
Assert.Equal([expected], clientLocal);
|
|
}
|
|
|
|
private static ClientCommandController NewController(
|
|
List<string>? calls = null,
|
|
List<uint>? errors = null,
|
|
uint? playerBitfield = 0x02000028u,
|
|
List<string>? messages = null,
|
|
List<string>? clientLocalMessages = 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,
|
|
Func<string, AcDream.Core.Chat.ChatLogResult>? chatLog = null,
|
|
Func<bool>? isPersistentDaylight = null,
|
|
Action<bool>? setPersistentDaylight = null,
|
|
Action<int>? setLandscapeRadius = null,
|
|
Action<float>? setFieldOfView = null)
|
|
{
|
|
calls ??= [];
|
|
errors ??= [];
|
|
messages ??= [];
|
|
clientLocalMessages ??= 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,
|
|
clientLocalMessages.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("log:" + name);
|
|
return chatLog?.Invoke(name)
|
|
?? new AcDream.Core.Chat.ChatLogResult(
|
|
Opened: name.Length > 0,
|
|
Closed: name.Length == 0,
|
|
Name: name,
|
|
ClosedName: "old.txt");
|
|
},
|
|
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"),
|
|
NewAdministrationBindings(calls),
|
|
isPersistentDaylight ?? (() => false),
|
|
setPersistentDaylight ?? (value => calls.Add("day:" + value)),
|
|
setLandscapeRadius ?? (value => calls.Add("radius:" + value)),
|
|
setFieldOfView ?? (value => calls.Add(
|
|
"fov:" + value.ToString(CultureInfo.InvariantCulture)))));
|
|
}
|
|
|
|
private static ClientCommandController.AdministrationBindings
|
|
NewAdministrationBindings(List<string> calls) => new(
|
|
BreakAllegianceBoot: (name, account) =>
|
|
calls.Add($"allegboot:{name}:{account}"),
|
|
AllegianceChatBoot: (name, reason) =>
|
|
calls.Add($"allegchatboot:{name}:{reason}"),
|
|
AllegianceChatGag: (name, enabled) =>
|
|
calls.Add($"allegchatgag:{name}:{enabled}"),
|
|
AllegianceBroadcast: text => calls.Add("allegbroadcast:" + text),
|
|
ListAllegianceBans: () => calls.Add("allegban:list"),
|
|
AddAllegianceBan: name => calls.Add("allegban:add:" + name),
|
|
RemoveAllegianceBan: name => calls.Add("allegban:remove:" + name),
|
|
ListAllegianceOfficers: () => calls.Add("allegofficer:list"),
|
|
ClearAllegianceOfficers: () => calls.Add("allegofficer:clear"),
|
|
SetAllegianceOfficer: (name, level) =>
|
|
calls.Add($"allegofficer:set:{level}:{name}"),
|
|
RemoveAllegianceOfficer: name =>
|
|
calls.Add("allegofficer:remove:" + name),
|
|
ListAllegianceOfficerTitles: () => calls.Add("allegtitle:list"),
|
|
ClearAllegianceOfficerTitles: () => calls.Add("allegtitle:clear"),
|
|
SetAllegianceOfficerTitle: (level, title) =>
|
|
calls.Add($"allegtitle:set:{level}:{title}"),
|
|
QueryAllegianceName: () => calls.Add("allegname:query"),
|
|
SetAllegianceName: name => calls.Add("allegname:set:" + name),
|
|
ClearAllegianceName: () => calls.Add("allegname:clear"),
|
|
AllegianceLockAction: action => calls.Add("alleglock:" + action),
|
|
SetAllegianceApprovedVassal: name =>
|
|
calls.Add("alleglock:bypass:" + name),
|
|
AllegianceHouseAction: action => calls.Add("alleghouse:" + action),
|
|
QueryMotd: () => calls.Add("motd:query"),
|
|
SetMotd: text => calls.Add("motd:set:" + text),
|
|
ClearMotd: () => calls.Add("motd:clear"),
|
|
SetOpenHouseStatus: open => calls.Add("houseopen:" + open),
|
|
AddPermanentGuest: name => calls.Add("houseguest:add:" + name),
|
|
RemovePermanentGuest: name => calls.Add("houseguest:remove:" + name),
|
|
RemoveAllPermanentGuests: () => calls.Add("houseguest:remove_all"),
|
|
ChangeStoragePermission: (name, enabled) =>
|
|
calls.Add($"housestorage:{enabled}:{name}"),
|
|
AddAllStoragePermission: () => calls.Add("housestorage:add_all"),
|
|
RemoveAllStoragePermission: () => calls.Add("housestorage:remove_all"),
|
|
RequestFullGuestList: () => calls.Add("houseguest:list"),
|
|
BootSpecificHouseGuest: name => calls.Add("houseboot:" + name),
|
|
BootEveryone: () => calls.Add("houseboot:all"),
|
|
SetHooksVisibility: visible => calls.Add("househooks:" + visible),
|
|
ModifyAllegianceGuestPermission: enabled =>
|
|
calls.Add("houseguest:allegiance:" + enabled),
|
|
ModifyAllegianceStoragePermission: enabled =>
|
|
calls.Add("housestorage:allegiance:" + enabled));
|
|
}
|