fix(headless): route wire-only chat commands
This commit is contained in:
parent
41b15efd4d
commit
259f0e5ac3
3 changed files with 219 additions and 0 deletions
|
|
@ -929,6 +929,24 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
case ClientCommandId.ClearChat:
|
||||
runtime.CommunicationOwner.Chat.Clear();
|
||||
return;
|
||||
case ClientCommandId.ChatToggle:
|
||||
// Retail DoChatToggle: "off" adds the global Speech
|
||||
// squelch; "on" removes it.
|
||||
session.SendModifyGlobalSquelch(
|
||||
command.Arguments.Equals(
|
||||
"off",
|
||||
StringComparison.OrdinalIgnoreCase),
|
||||
2u);
|
||||
return;
|
||||
case ClientCommandId.NoTellToggle:
|
||||
// Retail DoNoTell: "on" adds the global Tell squelch;
|
||||
// "off" removes it.
|
||||
session.SendModifyGlobalSquelch(
|
||||
command.Arguments.Equals(
|
||||
"on",
|
||||
StringComparison.OrdinalIgnoreCase),
|
||||
3u);
|
||||
return;
|
||||
case ClientCommandId.IndexChannels:
|
||||
session.SendIndexChannels();
|
||||
return;
|
||||
|
|
@ -953,6 +971,9 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
case ClientCommandId.AllegianceInfo:
|
||||
session.SendAllegianceInfoRequest(command.Arguments.Trim());
|
||||
return;
|
||||
case ClientCommandId.Permit:
|
||||
ExecutePermit(session, command.Arguments);
|
||||
return;
|
||||
case ClientCommandId.HouseAvailableList
|
||||
when RetailClientCommandCatalog.TryResolveHouseType(
|
||||
command.Arguments,
|
||||
|
|
@ -996,6 +1017,29 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
}
|
||||
send(channelId);
|
||||
}
|
||||
|
||||
static void ExecutePermit(
|
||||
AcDream.Core.Net.WorldSession activeSession,
|
||||
string arguments)
|
||||
{
|
||||
// The catalog has already required add/remove plus a name.
|
||||
// Match ClientCommandController's JoinArgsAsName behavior so
|
||||
// multi-word character names remain one exact wire argument.
|
||||
string[] parts = arguments.Split(
|
||||
(char[]?)null,
|
||||
StringSplitOptions.RemoveEmptyEntries);
|
||||
string name = string.Join(' ', parts, 1, parts.Length - 1);
|
||||
if (parts[0].Equals(
|
||||
"add",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
activeSession.SendAddPlayerPermission(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
activeSession.SendRemovePlayerPermission(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ILiveSessionEventRouting CreateEventRoute(
|
||||
|
|
|
|||
|
|
@ -54,6 +54,53 @@ public sealed class LiveSessionCommandRouterTests
|
|||
calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GraphicalRouteKeepsWireOnlyClientCommandParityWithHeadless()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
ClientCommandController.Bindings client = NewClientBindings() with
|
||||
{
|
||||
AddPlayerPermission = name =>
|
||||
calls.Add($"permit:add:{name}"),
|
||||
RemovePlayerPermission = name =>
|
||||
calls.Add($"permit:remove:{name}"),
|
||||
ModifyGlobalSquelch = (add, messageType) =>
|
||||
calls.Add($"squelch:{add}:{messageType}"),
|
||||
};
|
||||
var router = NewRouter(clientBindings: client);
|
||||
router.Activate();
|
||||
|
||||
router.Publish(new ExecuteClientCommandCmd(
|
||||
ClientCommandId.Permit,
|
||||
"add Aunt Agatha"));
|
||||
router.Publish(new ExecuteClientCommandCmd(
|
||||
ClientCommandId.Permit,
|
||||
"remove Lord Gnarly Beard"));
|
||||
router.Publish(new ExecuteClientCommandCmd(
|
||||
ClientCommandId.ChatToggle,
|
||||
"on"));
|
||||
router.Publish(new ExecuteClientCommandCmd(
|
||||
ClientCommandId.ChatToggle,
|
||||
"off"));
|
||||
router.Publish(new ExecuteClientCommandCmd(
|
||||
ClientCommandId.NoTellToggle,
|
||||
"on"));
|
||||
router.Publish(new ExecuteClientCommandCmd(
|
||||
ClientCommandId.NoTellToggle,
|
||||
"off"));
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
"permit:add:Aunt Agatha",
|
||||
"permit:remove:Lord Gnarly Beard",
|
||||
"squelch:False:2",
|
||||
"squelch:True:2",
|
||||
"squelch:True:3",
|
||||
"squelch:False:3",
|
||||
],
|
||||
calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InactiveAndDisposedRouter_CannotReachTransport()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -71,6 +71,127 @@ public sealed class HeadlessSessionHostTests
|
|||
BinaryPrimitives.ReadUInt32LittleEndian(captured[3].AsSpan(12)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoginCommandsRouteWireOnlyClientCommandsWithExactPolarityAndOrder()
|
||||
{
|
||||
var captured = new List<byte[]>();
|
||||
var operations = new FixtureSessionOperations
|
||||
{
|
||||
GameActionCapture = body => captured.Add(body),
|
||||
};
|
||||
using var diagnosticsOutput = new StringWriter();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(
|
||||
loginCommands:
|
||||
[
|
||||
"/permit add Aunt Agatha",
|
||||
"@permit remove Lord Gnarly Beard",
|
||||
"/chat on",
|
||||
"/chat off",
|
||||
"/notell on",
|
||||
"/notell off",
|
||||
],
|
||||
loginCommandDelayMs: 0),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(diagnosticsOutput),
|
||||
operations);
|
||||
|
||||
Assert.Equal(RuntimeSessionStartStatus.Connected, host.Start().Status);
|
||||
Assert.Equal(
|
||||
[
|
||||
ClientCommandRequests.AddPlayerPermissionOpcode,
|
||||
ClientCommandRequests.RemovePlayerPermissionOpcode,
|
||||
ClientCommandRequests.ModifyGlobalSquelchOpcode,
|
||||
ClientCommandRequests.ModifyGlobalSquelchOpcode,
|
||||
ClientCommandRequests.ModifyGlobalSquelchOpcode,
|
||||
ClientCommandRequests.ModifyGlobalSquelchOpcode,
|
||||
],
|
||||
captured.Select(ActionOpcode));
|
||||
Assert.Equal("Aunt Agatha", StringActionArgument(captured[0]));
|
||||
Assert.Equal("Lord Gnarly Beard", StringActionArgument(captured[1]));
|
||||
Assert.Equal(
|
||||
[
|
||||
(Add: 0u, MessageType: 2u),
|
||||
(Add: 1u, MessageType: 2u),
|
||||
(Add: 1u, MessageType: 3u),
|
||||
(Add: 0u, MessageType: 3u),
|
||||
],
|
||||
captured.Skip(2).Select(static body => (
|
||||
Add: BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
body.AsSpan(12, sizeof(uint))),
|
||||
MessageType: BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
body.AsSpan(16, sizeof(uint))))));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/permit add")]
|
||||
[InlineData("/chat maybe")]
|
||||
[InlineData("/notell maybe")]
|
||||
public void InvalidWireOnlyArgumentKeepsTypedFeedbackWithoutStatusFailure(
|
||||
string invalidCommand)
|
||||
{
|
||||
string statusPath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"acdream-headless-wire-client-errors-{Guid.NewGuid():N}.jsonl");
|
||||
try
|
||||
{
|
||||
var captured = new List<byte[]>();
|
||||
var operations = new FixtureSessionOperations
|
||||
{
|
||||
GameActionCapture = body => captured.Add(body),
|
||||
};
|
||||
using var diagnosticsOutput = new StringWriter();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(
|
||||
statusFile: statusPath,
|
||||
loginCommands:
|
||||
[
|
||||
invalidCommand,
|
||||
"after",
|
||||
],
|
||||
loginCommandDelayMs: 0),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(diagnosticsOutput),
|
||||
operations);
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Start().Status);
|
||||
Assert.Single(captured);
|
||||
Assert.Equal("after", TalkText(captured[0]));
|
||||
|
||||
// Invalid registered-command arguments are handled exactly as
|
||||
// typed: retail's ClientLocal refusal reaches canonical Runtime
|
||||
// feedback and is not misclassified as a transport failure.
|
||||
host.Runtime.CommunicationOwner.SpewBox.Tick(0d);
|
||||
Assert.Equal(
|
||||
"That is not a valid command.",
|
||||
Assert.Single(
|
||||
host.Runtime.CommunicationOwner.SpewBox.Snapshot()).Text);
|
||||
|
||||
JsonElement[] events = File.ReadAllLines(statusPath)
|
||||
.Select(static line =>
|
||||
JsonDocument.Parse(line).RootElement.Clone())
|
||||
.ToArray();
|
||||
Assert.DoesNotContain(
|
||||
events,
|
||||
static item => item.GetProperty("e").GetString()
|
||||
== "loginCommandFailed");
|
||||
Assert.True(host.Runtime.Session.IsInWorld);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(statusPath))
|
||||
File.Delete(statusPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoginCommandFailuresAreVersionedOrderedAndSessionIsolated()
|
||||
{
|
||||
|
|
@ -3286,6 +3407,13 @@ public sealed class HeadlessSessionHostTests
|
|||
return System.Text.Encoding.ASCII.GetString(body, 14, length);
|
||||
}
|
||||
|
||||
private static string StringActionArgument(byte[] body)
|
||||
{
|
||||
ushort length = BinaryPrimitives.ReadUInt16LittleEndian(
|
||||
body.AsSpan(12, sizeof(ushort)));
|
||||
return System.Text.Encoding.ASCII.GetString(body, 14, length);
|
||||
}
|
||||
|
||||
private sealed class ManualTimeProvider : TimeProvider
|
||||
{
|
||||
private long _timestamp;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue