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(); 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(); var errors = new List(); 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(); var errors = new List(); 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 MissingPlayerDescription_DoesNotInventAClientRejection() { var calls = new List(); 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(); var messages = new List(); 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(); 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(); var messages = new List(); 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(); var messages = new List(); 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(); 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(); var messages = new List(); 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(); 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(); var messages = new List(); 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); } [Fact] public void UnknownCommandId_FailsAtApplicationBoundary() { var controller = NewController(); var command = new ExecuteClientCommandCmd((ClientCommandId)999, string.Empty); Assert.Throws(() => controller.Execute(command)); } private static ClientCommandController NewController( List? calls = null, List? errors = null, uint? playerBitfield = 0x02000028u, List? messages = null, bool isAway = false, bool acceptsLootPermits = true, FriendsState? friends = null, SquelchState? squelch = null, string? lastTeller = null, bool vendorOpen = false) { 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}"); completed(true); }, () => 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}"))); } }